Add stats system with lifetime and session statistics, including line clear tracking

This commit is contained in:
cmclark00 2025-03-27 17:06:39 -04:00
parent 44c4c73feb
commit cdd80e5796
6 changed files with 547 additions and 4 deletions

View file

@ -21,6 +21,9 @@ import com.mintris.model.GameBoard
import com.mintris.audio.GameMusic import com.mintris.audio.GameMusic
import com.mintris.model.HighScoreManager import com.mintris.model.HighScoreManager
import android.content.Intent import android.content.Intent
import com.mintris.model.StatsManager
import java.text.SimpleDateFormat
import java.util.*
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@ -32,6 +35,7 @@ class MainActivity : AppCompatActivity() {
private lateinit var gameMusic: GameMusic private lateinit var gameMusic: GameMusic
private lateinit var titleScreen: TitleScreen private lateinit var titleScreen: TitleScreen
private lateinit var highScoreManager: HighScoreManager private lateinit var highScoreManager: HighScoreManager
private lateinit var statsManager: StatsManager
// Game state // Game state
private var isSoundEnabled = true private var isSoundEnabled = true
@ -40,6 +44,8 @@ class MainActivity : AppCompatActivity() {
private val maxLevel = 20 private val maxLevel = 20
private var currentScore = 0 private var currentScore = 0
private var currentLevel = 1 private var currentLevel = 1
private var gameStartTime: Long = 0
private var piecesPlaced: Int = 0
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -53,6 +59,7 @@ class MainActivity : AppCompatActivity() {
titleScreen = binding.titleScreen titleScreen = binding.titleScreen
gameMusic = GameMusic(this) gameMusic = GameMusic(this)
highScoreManager = HighScoreManager(this) highScoreManager = HighScoreManager(this)
statsManager = StatsManager(this)
// Set up game view // Set up game view
gameView.setGameBoard(gameBoard) gameView.setGameBoard(gameBoard)
@ -117,6 +124,8 @@ class MainActivity : AppCompatActivity() {
} else { } else {
android.util.Log.d("MainActivity", "Sound is disabled, skipping haptic feedback") android.util.Log.d("MainActivity", "Sound is disabled, skipping haptic feedback")
} }
// Record line clear in stats
statsManager.recordLineClear(lineCount)
} }
// Add callbacks for piece movement and locking // Add callbacks for piece movement and locking
@ -130,6 +139,7 @@ class MainActivity : AppCompatActivity() {
if (isSoundEnabled) { if (isSoundEnabled) {
gameHaptics.vibrateForPieceLock() gameHaptics.vibrateForPieceLock()
} }
piecesPlaced++
} }
// Set up button click listeners with haptic feedback // Set up button click listeners with haptic feedback
@ -187,6 +197,13 @@ class MainActivity : AppCompatActivity() {
} }
} }
// Set up stats button
binding.statsButton.setOnClickListener {
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
val intent = Intent(this, StatsActivity::class.java)
startActivity(intent)
}
// Initialize level selector // Initialize level selector
updateLevelSelector() updateLevelSelector()
@ -205,6 +222,9 @@ class MainActivity : AppCompatActivity() {
binding.linesText.text = lines.toString() binding.linesText.text = lines.toString()
binding.comboText.text = gameBoard.getCombo().toString() binding.comboText.text = gameBoard.getCombo().toString()
// Update current level for stats
currentLevel = level
// Force redraw of next piece preview // Force redraw of next piece preview
binding.nextPieceView.invalidate() binding.nextPieceView.invalidate()
} }
@ -213,7 +233,35 @@ class MainActivity : AppCompatActivity() {
* Show game over screen * Show game over screen
*/ */
private fun showGameOver(score: Int) { private fun showGameOver(score: Int) {
binding.finalScoreText.text = getString(R.string.score) + ": " + score val gameTime = System.currentTimeMillis() - gameStartTime
// Update session stats
statsManager.updateSessionStats(
score = score,
lines = gameBoard.lines,
pieces = piecesPlaced,
time = gameTime,
level = currentLevel
)
// End session and save stats
statsManager.endSession()
// Update session stats display
val timeFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
timeFormat.timeZone = TimeZone.getTimeZone("UTC")
binding.sessionScoreText.text = getString(R.string.session_score, score)
binding.sessionLinesText.text = getString(R.string.session_lines, gameBoard.lines)
binding.sessionPiecesText.text = getString(R.string.session_pieces, piecesPlaced)
binding.sessionTimeText.text = getString(R.string.session_time, timeFormat.format(gameTime))
binding.sessionLevelText.text = getString(R.string.session_level, currentLevel)
// Update session line clear stats
binding.sessionSinglesText.text = getString(R.string.singles, statsManager.getSessionSingles())
binding.sessionDoublesText.text = getString(R.string.doubles, statsManager.getSessionDoubles())
binding.sessionTriplesText.text = getString(R.string.triples, statsManager.getSessionTriples())
binding.sessionTetrisesText.text = getString(R.string.tetrises, statsManager.getSessionTetrises())
// Check if this is a high score // Check if this is a high score
if (highScoreManager.isHighScore(score)) { if (highScoreManager.isHighScore(score)) {
@ -291,10 +339,13 @@ class MainActivity : AppCompatActivity() {
private fun startGame() { private fun startGame() {
gameView.start() gameView.start()
gameMusic.setEnabled(isMusicEnabled) // Explicitly set enabled state gameMusic.setEnabled(isMusicEnabled)
if (isMusicEnabled) { if (isMusicEnabled) {
gameMusic.start() gameMusic.start()
} }
gameStartTime = System.currentTimeMillis()
piecesPlaced = 0
statsManager.startNewSession()
} }
private fun restartGame() { private fun restartGame() {

View file

@ -0,0 +1,54 @@
package com.mintris
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.mintris.databinding.ActivityStatsBinding
import com.mintris.model.StatsManager
import java.text.SimpleDateFormat
import java.util.*
class StatsActivity : AppCompatActivity() {
private lateinit var binding: ActivityStatsBinding
private lateinit var statsManager: StatsManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityStatsBinding.inflate(layoutInflater)
setContentView(binding.root)
statsManager = StatsManager(this)
// Set up back button
binding.backButton.setOnClickListener {
finish()
}
updateStats()
}
private fun updateStats() {
// Format time duration
val timeFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
timeFormat.timeZone = TimeZone.getTimeZone("UTC")
// Update lifetime stats
binding.totalGamesText.text = getString(R.string.total_games, statsManager.getTotalGames())
binding.totalScoreText.text = getString(R.string.total_score, statsManager.getTotalScore())
binding.totalLinesText.text = getString(R.string.total_lines, statsManager.getTotalLines())
binding.totalPiecesText.text = getString(R.string.total_pieces, statsManager.getTotalPieces())
binding.totalTimeText.text = getString(R.string.total_time, timeFormat.format(statsManager.getTotalTime()))
// Update line clear stats
binding.totalSinglesText.text = getString(R.string.singles, statsManager.getTotalSingles())
binding.totalDoublesText.text = getString(R.string.doubles, statsManager.getTotalDoubles())
binding.totalTriplesText.text = getString(R.string.triples, statsManager.getTotalTriples())
binding.totalTetrisesText.text = getString(R.string.tetrises, statsManager.getTotalTetrises())
// Update best performance stats
binding.maxLevelText.text = getString(R.string.max_level, statsManager.getMaxLevel())
binding.maxScoreText.text = getString(R.string.max_score, statsManager.getMaxScore())
binding.maxLinesText.text = getString(R.string.max_lines, statsManager.getMaxLines())
}
}

View file

@ -0,0 +1,175 @@
package com.mintris.model
import android.content.Context
import android.content.SharedPreferences
class StatsManager(context: Context) {
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
// Lifetime stats
private var totalGames: Int = 0
private var totalScore: Long = 0
private var totalLines: Int = 0
private var totalPieces: Int = 0
private var totalTime: Long = 0
private var maxLevel: Int = 0
private var maxScore: Int = 0
private var maxLines: Int = 0
// Line clear stats (lifetime)
private var totalSingles: Int = 0
private var totalDoubles: Int = 0
private var totalTriples: Int = 0
private var totalTetrises: Int = 0
// Session stats
private var sessionScore: Int = 0
private var sessionLines: Int = 0
private var sessionPieces: Int = 0
private var sessionTime: Long = 0
private var sessionLevel: Int = 0
// Line clear stats (session)
private var sessionSingles: Int = 0
private var sessionDoubles: Int = 0
private var sessionTriples: Int = 0
private var sessionTetrises: Int = 0
init {
loadStats()
}
private fun loadStats() {
totalGames = prefs.getInt(KEY_TOTAL_GAMES, 0)
totalScore = prefs.getLong(KEY_TOTAL_SCORE, 0)
totalLines = prefs.getInt(KEY_TOTAL_LINES, 0)
totalPieces = prefs.getInt(KEY_TOTAL_PIECES, 0)
totalTime = prefs.getLong(KEY_TOTAL_TIME, 0)
maxLevel = prefs.getInt(KEY_MAX_LEVEL, 0)
maxScore = prefs.getInt(KEY_MAX_SCORE, 0)
maxLines = prefs.getInt(KEY_MAX_LINES, 0)
// Load line clear stats
totalSingles = prefs.getInt(KEY_TOTAL_SINGLES, 0)
totalDoubles = prefs.getInt(KEY_TOTAL_DOUBLES, 0)
totalTriples = prefs.getInt(KEY_TOTAL_TRIPLES, 0)
totalTetrises = prefs.getInt(KEY_TOTAL_TETRISES, 0)
}
private fun saveStats() {
prefs.edit()
.putInt(KEY_TOTAL_GAMES, totalGames)
.putLong(KEY_TOTAL_SCORE, totalScore)
.putInt(KEY_TOTAL_LINES, totalLines)
.putInt(KEY_TOTAL_PIECES, totalPieces)
.putLong(KEY_TOTAL_TIME, totalTime)
.putInt(KEY_MAX_LEVEL, maxLevel)
.putInt(KEY_MAX_SCORE, maxScore)
.putInt(KEY_MAX_LINES, maxLines)
.putInt(KEY_TOTAL_SINGLES, totalSingles)
.putInt(KEY_TOTAL_DOUBLES, totalDoubles)
.putInt(KEY_TOTAL_TRIPLES, totalTriples)
.putInt(KEY_TOTAL_TETRISES, totalTetrises)
.apply()
}
fun startNewSession() {
sessionScore = 0
sessionLines = 0
sessionPieces = 0
sessionTime = 0
sessionLevel = 0
sessionSingles = 0
sessionDoubles = 0
sessionTriples = 0
sessionTetrises = 0
}
fun updateSessionStats(score: Int, lines: Int, pieces: Int, time: Long, level: Int) {
sessionScore = score
sessionLines = lines
sessionPieces = pieces
sessionTime = time
sessionLevel = level
}
fun recordLineClear(lineCount: Int) {
when (lineCount) {
1 -> {
sessionSingles++
totalSingles++
}
2 -> {
sessionDoubles++
totalDoubles++
}
3 -> {
sessionTriples++
totalTriples++
}
4 -> {
sessionTetrises++
totalTetrises++
}
}
}
fun endSession() {
totalGames++
totalScore += sessionScore
totalLines += sessionLines
totalPieces += sessionPieces
totalTime += sessionTime
if (sessionLevel > maxLevel) maxLevel = sessionLevel
if (sessionScore > maxScore) maxScore = sessionScore
if (sessionLines > maxLines) maxLines = sessionLines
saveStats()
}
// Getters for lifetime stats
fun getTotalGames(): Int = totalGames
fun getTotalScore(): Long = totalScore
fun getTotalLines(): Int = totalLines
fun getTotalPieces(): Int = totalPieces
fun getTotalTime(): Long = totalTime
fun getMaxLevel(): Int = maxLevel
fun getMaxScore(): Int = maxScore
fun getMaxLines(): Int = maxLines
// Getters for line clear stats (lifetime)
fun getTotalSingles(): Int = totalSingles
fun getTotalDoubles(): Int = totalDoubles
fun getTotalTriples(): Int = totalTriples
fun getTotalTetrises(): Int = totalTetrises
// Getters for session stats
fun getSessionScore(): Int = sessionScore
fun getSessionLines(): Int = sessionLines
fun getSessionPieces(): Int = sessionPieces
fun getSessionTime(): Long = sessionTime
fun getSessionLevel(): Int = sessionLevel
// Getters for line clear stats (session)
fun getSessionSingles(): Int = sessionSingles
fun getSessionDoubles(): Int = sessionDoubles
fun getSessionTriples(): Int = sessionTriples
fun getSessionTetrises(): Int = sessionTetrises
companion object {
private const val PREFS_NAME = "mintris_stats"
private const val KEY_TOTAL_GAMES = "total_games"
private const val KEY_TOTAL_SCORE = "total_score"
private const val KEY_TOTAL_LINES = "total_lines"
private const val KEY_TOTAL_PIECES = "total_pieces"
private const val KEY_TOTAL_TIME = "total_time"
private const val KEY_MAX_LEVEL = "max_level"
private const val KEY_MAX_SCORE = "max_score"
private const val KEY_MAX_LINES = "max_lines"
private const val KEY_TOTAL_SINGLES = "total_singles"
private const val KEY_TOTAL_DOUBLES = "total_doubles"
private const val KEY_TOTAL_TRIPLES = "total_triples"
private const val KEY_TOTAL_TETRISES = "total_tetrises"
}
}

View file

@ -135,10 +135,92 @@
android:textStyle="bold" /> android:textStyle="bold" />
<TextView <TextView
android:id="@+id/finalScoreText"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:text="@string/session_stats"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginTop="24dp" />
<TextView
android:id="@+id/sessionScoreText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="@color/white"
android:textSize="18sp" />
<TextView
android:id="@+id/sessionLinesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/white"
android:textSize="18sp" />
<TextView
android:id="@+id/sessionPiecesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/white"
android:textSize="18sp" />
<TextView
android:id="@+id/sessionTimeText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/white"
android:textSize="18sp" />
<TextView
android:id="@+id/sessionLevelText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/white"
android:textSize="18sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/line_clears"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginTop="16dp" />
<TextView
android:id="@+id/sessionSinglesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/white"
android:textSize="18sp" />
<TextView
android:id="@+id/sessionDoublesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/white"
android:textSize="18sp" />
<TextView
android:id="@+id/sessionTriplesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/white"
android:textSize="18sp" />
<TextView
android:id="@+id/sessionTetrisesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="18sp" /> android:textSize="18sp" />
@ -209,6 +291,16 @@
android:text="@string/high_scores" android:text="@string/high_scores"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="18sp" /> android:textSize="18sp" />
<Button
android:id="@+id/statsButton"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@color/transparent"
android:text="@string/stats"
android:textColor="@color/white"
android:textSize="18sp" />
<LinearLayout <LinearLayout
android:id="@+id/levelSelectorContainer" android:id="@+id/levelSelectorContainer"

View file

@ -0,0 +1,147 @@
<?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"
android:background="@color/black">
<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="@string/lifetime_stats"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginBottom="24dp"/>
<TextView
android:id="@+id/totalGamesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="18sp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/totalScoreText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="18sp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/totalLinesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="18sp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/totalPiecesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="18sp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/totalTimeText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="18sp"
android:layout_marginBottom="24dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/line_clears"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"/>
<TextView
android:id="@+id/totalSinglesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="18sp"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/totalDoublesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="18sp"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/totalTriplesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="18sp"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/totalTetrisesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="18sp"
android:layout_marginBottom="24dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/best_performance"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginBottom="24dp"/>
<TextView
android:id="@+id/maxLevelText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="18sp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/maxScoreText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="18sp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/maxLinesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="18sp"
android:layout_marginBottom="24dp"/>
<Button
android:id="@+id/backButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/back"
android:textColor="@color/white"
android:background="@color/transparent"
android:layout_marginTop="16dp"/>
</LinearLayout>
</ScrollView>

View file

@ -20,4 +20,28 @@
<string name="new_high_score">New High Score!</string> <string name="new_high_score">New High Score!</string>
<string name="save">Save</string> <string name="save">Save</string>
<string name="back">Back</string> <string name="back">Back</string>
<!-- Stats Screen -->
<string name="lifetime_stats">Lifetime Stats</string>
<string name="best_performance">Best Performance</string>
<string name="total_games">Total Games: %d</string>
<string name="total_score">Total Score: %d</string>
<string name="total_lines">Total Lines: %d</string>
<string name="total_pieces">Total Pieces: %d</string>
<string name="total_time">Total Time: %s</string>
<string name="max_level">Max Level: %d</string>
<string name="max_score">Max Score: %d</string>
<string name="max_lines">Max Lines: %d</string>
<string name="stats">Stats</string>
<string name="session_stats">Session Stats</string>
<string name="session_score">Score: %d</string>
<string name="session_lines">Lines: %d</string>
<string name="session_pieces">Pieces: %d</string>
<string name="session_time">Time: %s</string>
<string name="session_level">Level: %d</string>
<string name="line_clears">Line Clears</string>
<string name="singles">Singles: %d</string>
<string name="doubles">Doubles: %d</string>
<string name="triples">Triples: %d</string>
<string name="tetrises">Tetrises: %d</string>
</resources> </resources>