Implement automatic score analysis with dynamic learning. Removed Analysis tab and integrated automatic equivalence calculation after score entry with mandatory thresholds (3+ scores, 2+ games) and showing only played games.

This commit is contained in:
cmclark00 2025-03-24 17:01:26 -04:00
parent 7fa9e2a12d
commit 857331566e
11 changed files with 558 additions and 155 deletions

View file

@ -88,6 +88,9 @@ dependencies {
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")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")

View file

@ -2,12 +2,16 @@ package com.accidentalproductions.tetristats
import android.app.Application
import androidx.room.Room
import com.accidentalproductions.tetristats.data.ScalingFactorsManager
import com.accidentalproductions.tetristats.data.ScoreDatabase
class TetriStatsApplication : Application() {
lateinit var database: ScoreDatabase
private set
lateinit var scalingFactorsManager: ScalingFactorsManager
private set
override fun onCreate() {
super.onCreate()
try {
@ -31,5 +35,8 @@ class TetriStatsApplication : Application() {
.fallbackToDestructiveMigration()
.build()
}
// Initialize the ScalingFactorsManager
scalingFactorsManager = ScalingFactorsManager(applicationContext)
}
}

View file

@ -0,0 +1,166 @@
package com.accidentalproductions.tetristats.data
import android.content.Context
import android.content.SharedPreferences
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* Manages the learning and persistence of scaling factors based on user input
*/
class ScalingFactorsManager(context: Context) {
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
private val gson = Gson()
// In-memory cache of learned factors
private var learnedFactors: MutableMap<String, MutableMap<String, LearningData>> = 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<String, MutableMap<String, LearningData>> {
val json = prefs.getString(KEY_LEARNED_FACTORS, null) ?: return mutableMapOf()
val type = object : TypeToken<MutableMap<String, MutableMap<String, LearningData>>>() {}.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"
}
}

View file

@ -15,6 +15,9 @@ interface ScoreDao {
@Query("SELECT DISTINCT gameVersion FROM scores")
fun getGamesWithScores(): LiveData<List<String>>
@Query("SELECT COUNT(*) FROM scores")
fun getTotalScoreCount(): LiveData<Int>
@Query("SELECT AVG(scoreValue) FROM scores WHERE gameVersion = :gameVersion")
fun getAverageScore(gameVersion: String): LiveData<Double>

View file

@ -5,8 +5,10 @@ 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
@ -14,6 +16,7 @@ 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
override fun onCreateView(
inflater: LayoutInflater,
@ -28,8 +31,12 @@ class EntryFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
setupGameVersionDropdown()
setupScoreConverter()
setupRecyclerView()
setupSubmitButton()
setupAutoAnalysis()
// Check if we should show conversions on startup
viewModel.checkConversionCriteria()
}
private fun setupGameVersionDropdown() {
@ -46,58 +53,102 @@ class EntryFragment : Fragment() {
binding.autoCompleteGameVersion.setAdapter(adapter)
}
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 setupRecyclerView() {
equivalentScoreAdapter = EquivalentScoreAdapter()
binding.recyclerViewEquivalentScores.apply {
adapter = equivalentScoreAdapter
layoutManager = LinearLayoutManager(context)
}
}
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)
private fun setupAutoAnalysis() {
// Hide the analysis card by default
binding.cardAnalysisResults.visibility = View.GONE
binding.spinnerScoreSelect.setOnItemClickListener { _, _, position, _ ->
viewModel.setSelectedScore(scores[position])
// Observe if we should show conversions
viewModel.showConversion.observe(viewLifecycleOwner) { shouldShow ->
if (!shouldShow) {
// Hide analysis card if we don't meet criteria
binding.cardAnalysisResults.visibility = View.GONE
// Also show a message that not enough scores have been entered
if (viewModel.lastSubmittedGame.value != null) {
Toast.makeText(
context,
"Enter at least 3 scores across 2 different games to see conversions",
Toast.LENGTH_LONG
).show()
}
}
}
// 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()) {
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, games)
binding.autoCompleteEquivalentGame.setAdapter(adapter)
}
}
// 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) { game ->
// Only continue if showConversion is true
if (viewModel.showConversion.value != true) return@observe
viewModel.lastSubmittedScore.value?.let { score ->
binding.textViewOriginalScore.text = "Your $game score of ${"%,d".format(score)} is equivalent to:"
binding.cardAnalysisResults.visibility = View.VISIBLE
// 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()) {
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
Toast.makeText(
context,
"Add scores for other games to see conversions",
Toast.LENGTH_LONG
).show()
}
}
}
// Observe equivalent scores
viewModel.equivalentScores.observe(viewLifecycleOwner) { scores ->
if (scores.isNotEmpty()) {
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
}
}
}
@ -119,6 +170,15 @@ class EntryFragment : Fragment() {
linesCleared = linesCleared
)
clearInputs()
// Only scroll down if we're going to show conversions
if (viewModel.showConversion.value == true) {
binding.root.post {
binding.root.fullScroll(View.FOCUS_DOWN)
}
}
} else {
Toast.makeText(context, "Please enter a game and score", Toast.LENGTH_SHORT).show()
}
}
}

View file

@ -2,48 +2,65 @@ package com.accidentalproductions.tetristats.ui.entry
import android.app.Application
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"
)
// Track user played games and score counts
val gamesWithScores = scoreDao.getGamesWithScores()
val totalScoreCount = scoreDao.getTotalScoreCount()
private val _selectedFromGame = MutableLiveData<String>()
private val _selectedScore = MutableLiveData<Score>()
private val _selectedToGame = MutableLiveData<String>()
private val _convertedScore = MutableLiveData<Int>()
// For auto-analysis
private val _lastSubmittedGame = MutableLiveData<String>()
private val _lastSubmittedScore = MutableLiveData<Int>()
private val _equivalentScores = MutableLiveData<List<EquivalentScore>>()
private val _showConversion = MutableLiveData<Boolean>(false)
val convertedScore: LiveData<Int> = _convertedScore
// Current game selection for learning
private val _selectedEquivalentGame = MutableLiveData<String>()
val equivalentScores: LiveData<List<EquivalentScore>> = _equivalentScores
val lastSubmittedGame: LiveData<String> = _lastSubmittedGame
val lastSubmittedScore: LiveData<Int> = _lastSubmittedScore
val showConversion: LiveData<Boolean> = _showConversion
fun getScoresForGame(gameVersion: String): LiveData<List<Score>> {
return scoreDao.getScoresForGame(gameVersion)
}
fun setSelectedFromGame(game: String) {
_selectedFromGame.value = game
fun setSelectedEquivalentGame(game: String) {
_selectedEquivalentGame.value = game
}
fun setSelectedScore(score: Score) {
_selectedScore.value = score
}
/**
* Check if we should show conversions based on score count and game count
*/
fun checkConversionCriteria() {
viewModelScope.launch {
val scoreCount = totalScoreCount.value ?: 0
val gameCount = gamesWithScores.value?.size ?: 0
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
// Only show conversions if there are at least 3 scores across at least 2 games
_showConversion.postValue(scoreCount >= 3 && gameCount >= 2)
}
}
@ -63,6 +80,65 @@ class EntryViewModel(application: Application) : AndroidViewModel(application) {
linesCleared = linesCleared
)
scoreDao.insert(newScore)
// After inserting, update the last submitted values
_lastSubmittedGame.postValue(gameVersion)
_lastSubmittedScore.postValue(score)
// Check if we should show conversions
checkConversionCriteria()
// Only generate equivalent scores if we meet the criteria
if (_showConversion.value == true) {
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<EquivalentScore>()
// 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
)
)
}
}
_equivalentScores.postValue(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)
}
}
}
}

View file

@ -0,0 +1,55 @@
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<EquivalentScore, EquivalentScoreAdapter.ViewHolder>(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<EquivalentScore>() {
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
)

View file

@ -112,10 +112,12 @@
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Score Converter Card -->
<!-- Automatic Analysis Card (hidden by default, shown after submission) -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardAnalysisResults"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:cardElevation="4dp">
<LinearLayout
@ -127,92 +129,81 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Score Converter"
android:text="Equivalent Scores in Other Games"
android:textAppearance="?attr/textAppearanceHeadline6"
android:layout_marginBottom="16dp"/>
<com.google.android.material.textfield.TextInputLayout
<TextView
android:id="@+id/textViewOriginalScore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceSubtitle1"
android:layout_marginBottom="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
tools:text="Your NES Tetris score of 500,000 is equivalent to:"/>
<AutoCompleteTextView
android:id="@+id/autoCompleteFromGame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="From Game"
android:inputType="none"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewEquivalentScores"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
android:layout_marginBottom="8dp"/>
<AutoCompleteTextView
android:id="@+id/spinnerScoreSelect"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Select Score"
android:inputType="none"/>
</com.google.android.material.textfield.TextInputLayout>
<!-- Learning feedback section -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Improve Conversions"
android:textAppearance="?attr/textAppearanceSubtitle1"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"/>
<com.google.android.material.textfield.TextInputLayout
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
android:text="Do you know the equivalent score in another game? Add it to help improve future conversions:"
android:textAppearance="?attr/textAppearanceBody2"
android:layout_marginBottom="8dp"/>
<AutoCompleteTextView
android:id="@+id/autoCompleteToGame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="To Game"
android:inputType="none"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/buttonConvert"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Convert Score"
android:layout_marginBottom="16dp"/>
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardConvertedScore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:cardBackgroundColor="@color/tetris_turquoise"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
<com.google.android.material.textfield.TextInputLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
android:layout_weight="1"
android:layout_marginEnd="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Converted Score"
android:textColor="@color/tetris_navy"
android:textAppearance="?attr/textAppearanceBody1"/>
<TextView
android:id="@+id/textViewConvertedScore"
<AutoCompleteTextView
android:id="@+id/autoCompleteEquivalentGame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:textColor="@color/tetris_navy"
android:textAppearance="?attr/textAppearanceHeadline4"
tools:text="1,000,000"/>
android:hint="Game"
android:inputType="none"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextEquivalentScore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Score"
android:inputType="number"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/buttonAddEquivalent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Add Equivalent Score"/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:cardElevation="2dp"
app:cardCornerRadius="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/textViewGameName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="?attr/textAppearanceSubtitle1"
android:textColor="@color/tetris_turquoise"
tools:text="Tetris DS" />
<TextView
android:id="@+id/textViewEquivalentScore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceHeadline6"
android:textStyle="bold"
android:textColor="@color/tetris_navy"
tools:text="1,500,000" />
</LinearLayout>
<TextView
android:id="@+id/textViewSampleCount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceCaption"
android:textStyle="italic"
android:textColor="@android:color/darker_gray"
android:layout_marginTop="4dp"
tools:text="Based on 3 learning samples" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -16,9 +16,4 @@
android:icon="@drawable/ic_stats_24"
android:title="Stats" />
<item
android:id="@+id/navigation_analysis"
android:icon="@drawable/ic_analytics_24"
android:title="Analysis" />
</menu>

View file

@ -23,10 +23,4 @@
android:label="Statistics"
tools:layout="@layout/fragment_stats" />
<fragment
android:id="@+id/navigation_analysis"
android:name="com.accidentalproductions.tetristats.ui.analysis.ScalingFactorFragment"
android:label="Scaling Analysis"
tools:layout="@layout/fragment_scaling_factor" />
</navigation>