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

@ -87,6 +87,9 @@ dependencies {
implementation("androidx.room:room-runtime:2.6.1") implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1") implementation("androidx.room:room-ktx:2.6.1")
ksp("androidx.room:room-compiler: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") testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.ext:junit:1.1.5")

View file

@ -2,11 +2,15 @@ package com.accidentalproductions.tetristats
import android.app.Application import android.app.Application
import androidx.room.Room import androidx.room.Room
import com.accidentalproductions.tetristats.data.ScalingFactorsManager
import com.accidentalproductions.tetristats.data.ScoreDatabase import com.accidentalproductions.tetristats.data.ScoreDatabase
class TetriStatsApplication : Application() { class TetriStatsApplication : Application() {
lateinit var database: ScoreDatabase lateinit var database: ScoreDatabase
private set private set
lateinit var scalingFactorsManager: ScalingFactorsManager
private set
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -31,5 +35,8 @@ class TetriStatsApplication : Application() {
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.build() .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") @Query("SELECT DISTINCT gameVersion FROM scores")
fun getGamesWithScores(): LiveData<List<String>> fun getGamesWithScores(): LiveData<List<String>>
@Query("SELECT COUNT(*) FROM scores")
fun getTotalScoreCount(): LiveData<Int>
@Query("SELECT AVG(scoreValue) FROM scores WHERE gameVersion = :gameVersion") @Query("SELECT AVG(scoreValue) FROM scores WHERE gameVersion = :gameVersion")
fun getAverageScore(gameVersion: String): LiveData<Double> fun getAverageScore(gameVersion: String): LiveData<Double>

View file

@ -5,8 +5,10 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.accidentalproductions.tetristats.data.Score import com.accidentalproductions.tetristats.data.Score
import com.accidentalproductions.tetristats.databinding.FragmentEntryBinding import com.accidentalproductions.tetristats.databinding.FragmentEntryBinding
@ -14,6 +16,7 @@ class EntryFragment : Fragment() {
private var _binding: FragmentEntryBinding? = null private var _binding: FragmentEntryBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private val viewModel: EntryViewModel by viewModels { EntryViewModelFactory(requireActivity().application) } private val viewModel: EntryViewModel by viewModels { EntryViewModelFactory(requireActivity().application) }
private lateinit var equivalentScoreAdapter: EquivalentScoreAdapter
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -28,8 +31,12 @@ class EntryFragment : Fragment() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setupGameVersionDropdown() setupGameVersionDropdown()
setupScoreConverter() setupRecyclerView()
setupSubmitButton() setupSubmitButton()
setupAutoAnalysis()
// Check if we should show conversions on startup
viewModel.checkConversionCriteria()
} }
private fun setupGameVersionDropdown() { private fun setupGameVersionDropdown() {
@ -45,59 +52,103 @@ class EntryFragment : Fragment() {
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, games) val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, games)
binding.autoCompleteGameVersion.setAdapter(adapter) binding.autoCompleteGameVersion.setAdapter(adapter)
} }
private fun setupScoreConverter() { private fun setupRecyclerView() {
// Setup "From Game" dropdown with games that have scores equivalentScoreAdapter = EquivalentScoreAdapter()
viewModel.gamesWithScores.observe(viewLifecycleOwner) { games -> binding.recyclerViewEquivalentScores.apply {
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, games) adapter = equivalentScoreAdapter
binding.autoCompleteFromGame.setAdapter(adapter) layoutManager = LinearLayoutManager(context)
}
// 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 updateScoreDropdown(gameVersion: String) { private fun setupAutoAnalysis() {
viewModel.getScoresForGame(gameVersion).observe(viewLifecycleOwner) { scores -> // Hide the analysis card by default
val scoreStrings = scores.map { "${it.scoreValue} (Level ${it.endLevel ?: "?"})"} binding.cardAnalysisResults.visibility = View.GONE
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, scoreStrings)
binding.spinnerScoreSelect.setAdapter(adapter) // Observe if we should show conversions
viewModel.showConversion.observe(viewLifecycleOwner) { shouldShow ->
binding.spinnerScoreSelect.setOnItemClickListener { _, _, position, _ -> if (!shouldShow) {
viewModel.setSelectedScore(scores[position]) // 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 linesCleared = linesCleared
) )
clearInputs() 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 android.app.Application
import androidx.lifecycle.* import androidx.lifecycle.*
import com.accidentalproductions.tetristats.TetriStatsApplication
import com.accidentalproductions.tetristats.data.Score import com.accidentalproductions.tetristats.data.Score
import com.accidentalproductions.tetristats.data.ScoreDatabase import com.accidentalproductions.tetristats.data.ScoreDatabase
import com.accidentalproductions.tetristats.data.ScalingFactors import com.accidentalproductions.tetristats.data.ScalingFactors
import com.accidentalproductions.tetristats.data.ScalingFactorsManager
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class EntryViewModel(application: Application) : AndroidViewModel(application) { class EntryViewModel(application: Application) : AndroidViewModel(application) {
private val database = ScoreDatabase.getDatabase(application) private val database = ScoreDatabase.getDatabase(application)
private val scoreDao = database.scoreDao() private val scoreDao = database.scoreDao()
private val scalingFactorsManager = (application as TetriStatsApplication).scalingFactorsManager
val gamesWithScores = scoreDao.getGamesWithScores() // All games list for reference
private val allGames = listOf(
"NES Tetris",
"Game Boy Tetris",
"Tetris DX",
"Tetris DS",
"Tetris Effect",
"Rosy Retrospection DX",
"Apotris"
)
private val _selectedFromGame = MutableLiveData<String>() // Track user played games and score counts
private val _selectedScore = MutableLiveData<Score>() val gamesWithScores = scoreDao.getGamesWithScores()
private val _selectedToGame = MutableLiveData<String>() val totalScoreCount = scoreDao.getTotalScoreCount()
private val _convertedScore = MutableLiveData<Int>()
// For auto-analysis
val convertedScore: LiveData<Int> = _convertedScore private val _lastSubmittedGame = MutableLiveData<String>()
private val _lastSubmittedScore = MutableLiveData<Int>()
private val _equivalentScores = MutableLiveData<List<EquivalentScore>>()
private val _showConversion = MutableLiveData<Boolean>(false)
// 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>> { fun getScoresForGame(gameVersion: String): LiveData<List<Score>> {
return scoreDao.getScoresForGame(gameVersion) return scoreDao.getScoresForGame(gameVersion)
} }
fun setSelectedFromGame(game: String) { fun setSelectedEquivalentGame(game: String) {
_selectedFromGame.value = game _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() {
fun setSelectedToGame(game: String) { viewModelScope.launch {
_selectedToGame.value = game val scoreCount = totalScoreCount.value ?: 0
} val gameCount = gamesWithScores.value?.size ?: 0
fun convertScore() { // Only show conversions if there are at least 3 scores across at least 2 games
val fromGame = _selectedFromGame.value _showConversion.postValue(scoreCount >= 3 && gameCount >= 2)
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
} }
} }
@ -63,6 +80,65 @@ class EntryViewModel(application: Application) : AndroidViewModel(application) {
linesCleared = linesCleared linesCleared = linesCleared
) )
scoreDao.insert(newScore) 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> </LinearLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
<!-- Score Converter Card --> <!-- Automatic Analysis Card (hidden by default, shown after submission) -->
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:id="@+id/cardAnalysisResults"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="gone"
app:cardElevation="4dp"> app:cardElevation="4dp">
<LinearLayout <LinearLayout
@ -127,92 +129,81 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Score Converter" android:text="Equivalent Scores in Other Games"
android:textAppearance="?attr/textAppearanceHeadline6" android:textAppearance="?attr/textAppearanceHeadline6"
android:layout_marginBottom="16dp"/> android:layout_marginBottom="16dp"/>
<com.google.android.material.textfield.TextInputLayout <TextView
android:id="@+id/textViewOriginalScore"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceSubtitle1"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"> tools:text="Your NES Tetris score of 500,000 is equivalent to:"/>
<AutoCompleteTextView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/autoCompleteFromGame" android:id="@+id/recyclerViewEquivalentScores"
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
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"/>
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
<AutoCompleteTextView <!-- Learning feedback section -->
android:id="@+id/spinnerScoreSelect" <TextView
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="Select Score" android:text="Improve Conversions"
android:inputType="none"/> android:textAppearance="?attr/textAppearanceSubtitle1"
</com.google.android.material.textfield.TextInputLayout> android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"/>
<com.google.android.material.textfield.TextInputLayout
<TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp" android:text="Do you know the equivalent score in another game? Add it to help improve future conversions:"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"> android:textAppearance="?attr/textAppearanceBody2"
android:layout_marginBottom="8dp"/>
<AutoCompleteTextView <LinearLayout
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"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Convert Score" android:orientation="horizontal"
android:layout_marginBottom="16dp"/> android:layout_marginBottom="8dp">
<com.google.android.material.card.MaterialCardView <com.google.android.material.textfield.TextInputLayout
android:id="@+id/cardConvertedScore" android:layout_width="0dp"
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"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:layout_weight="1"
android:padding="16dp"> android:layout_marginEnd="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
<TextView <AutoCompleteTextView
android:layout_width="wrap_content" android:id="@+id/autoCompleteEquivalentGame"
android:layout_height="wrap_content"
android:text="Converted Score"
android:textColor="@color/tetris_navy"
android:textAppearance="?attr/textAppearanceBody1"/>
<TextView
android:id="@+id/textViewConvertedScore"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:hint="Game"
android:gravity="center" android:inputType="none"/>
android:textColor="@color/tetris_navy" </com.google.android.material.textfield.TextInputLayout>
android:textAppearance="?attr/textAppearanceHeadline4"
tools:text="1,000,000"/>
</LinearLayout> <com.google.android.material.textfield.TextInputLayout
</com.google.android.material.card.MaterialCardView> 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> </LinearLayout>
</com.google.android.material.card.MaterialCardView> </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:icon="@drawable/ic_stats_24"
android:title="Stats" /> android:title="Stats" />
<item
android:id="@+id/navigation_analysis"
android:icon="@drawable/ic_analytics_24"
android:title="Analysis" />
</menu> </menu>

View file

@ -23,10 +23,4 @@
android:label="Statistics" android:label="Statistics"
tools:layout="@layout/fragment_stats" /> 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> </navigation>