mirror of
https://github.com/cmclark00/TetriStats.git
synced 2025-05-17 22:55:21 +01:00
Add scaling factor analysis tool for calculating game score conversions
This commit is contained in:
parent
53a9d84f13
commit
a79783d712
4 changed files with 388 additions and 0 deletions
|
@ -32,6 +32,11 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ui.ScalingFactorTestActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="Scaling Factor Analysis"/>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.provider"
|
android:authorities="${applicationId}.provider"
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
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<GameScoreSample>()
|
||||||
|
private val baseGame = "NES Tetris"
|
||||||
|
|
||||||
|
val sampleCount: Int
|
||||||
|
get() = samples.size
|
||||||
|
|
||||||
|
fun addSample(sample: GameScoreSample) {
|
||||||
|
samples.add(sample)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addSamples(newSamples: List<GameScoreSample>) {
|
||||||
|
samples.addAll(newSamples)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearSamples() {
|
||||||
|
samples.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun analyzeScoringCurves(): Map<String, RangeScalingFactor> {
|
||||||
|
val groupedSamples = samples.groupBy { it.game }
|
||||||
|
val conversionFactors = mutableMapOf<String, RangeScalingFactor>()
|
||||||
|
|
||||||
|
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<GameScoreSample>, 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(")
|
||||||
|
|
||||||
|
games.filter { it != game }.forEach { otherGame ->
|
||||||
|
val factor = factors[otherGame] ?: RangeScalingFactor(1.0, 1.0, 1.0)
|
||||||
|
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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
160
app/src/main/res/layout/activity_scaling_factor_test.xml
Normal file
160
app/src/main/res/layout/activity_scaling_factor_test.xml
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Scaling Factor Analysis Tool"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="16dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textViewSampleCount"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Samples: 0"
|
||||||
|
android:layout_marginBottom="16dp"/>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="Game"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<AutoCompleteTextView
|
||||||
|
android:id="@+id/spinnerGame"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="none"/>
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="Skill Level"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<AutoCompleteTextView
|
||||||
|
android:id="@+id/spinnerSkillLevel"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="none"/>
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="Score"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/editTextScore"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number"/>
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="Level"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/editTextLevel"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number"/>
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="Notes (optional)"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/editTextNotes"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textMultiLine"
|
||||||
|
android:minLines="2"/>
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/buttonAddSample"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Add Sample"
|
||||||
|
android:layout_marginEnd="8dp"/>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/buttonClear"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Clear All"
|
||||||
|
android:layout_marginStart="8dp"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/buttonAnalyze"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Analyze"
|
||||||
|
android:layout_marginBottom="16dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Analysis Report"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="8dp"/>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@android:color/darker_gray"
|
||||||
|
android:padding="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textViewReport"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:fontFamily="monospace"/>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
Loading…
Add table
Add a link
Reference in a new issue