diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index bc5761a..aaa37f3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -28,7 +28,12 @@
+ android:exported="false"
+ android:theme="@style/Theme.AppCompat.NoActionBar" />
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/mintris/MainActivity.kt b/app/src/main/java/com/mintris/MainActivity.kt
index d6eac7e..fd49615 100644
--- a/app/src/main/java/com/mintris/MainActivity.kt
+++ b/app/src/main/java/com/mintris/MainActivity.kt
@@ -21,6 +21,9 @@ import com.mintris.model.GameBoard
import com.mintris.audio.GameMusic
import com.mintris.model.HighScoreManager
import android.content.Intent
+import com.mintris.model.StatsManager
+import java.text.SimpleDateFormat
+import java.util.*
class MainActivity : AppCompatActivity() {
@@ -32,6 +35,7 @@ class MainActivity : AppCompatActivity() {
private lateinit var gameMusic: GameMusic
private lateinit var titleScreen: TitleScreen
private lateinit var highScoreManager: HighScoreManager
+ private lateinit var statsManager: StatsManager
// Game state
private var isSoundEnabled = true
@@ -40,6 +44,8 @@ class MainActivity : AppCompatActivity() {
private val maxLevel = 20
private var currentScore = 0
private var currentLevel = 1
+ private var gameStartTime: Long = 0
+ private var piecesPlaced: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -53,6 +59,7 @@ class MainActivity : AppCompatActivity() {
titleScreen = binding.titleScreen
gameMusic = GameMusic(this)
highScoreManager = HighScoreManager(this)
+ statsManager = StatsManager(this)
// Set up game view
gameView.setGameBoard(gameBoard)
@@ -117,6 +124,8 @@ class MainActivity : AppCompatActivity() {
} else {
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
@@ -130,6 +139,7 @@ class MainActivity : AppCompatActivity() {
if (isSoundEnabled) {
gameHaptics.vibrateForPieceLock()
}
+ piecesPlaced++
}
// 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
updateLevelSelector()
@@ -205,6 +222,9 @@ class MainActivity : AppCompatActivity() {
binding.linesText.text = lines.toString()
binding.comboText.text = gameBoard.getCombo().toString()
+ // Update current level for stats
+ currentLevel = level
+
// Force redraw of next piece preview
binding.nextPieceView.invalidate()
}
@@ -213,7 +233,35 @@ class MainActivity : AppCompatActivity() {
* Show game over screen
*/
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
if (highScoreManager.isHighScore(score)) {
@@ -291,10 +339,13 @@ class MainActivity : AppCompatActivity() {
private fun startGame() {
gameView.start()
- gameMusic.setEnabled(isMusicEnabled) // Explicitly set enabled state
+ gameMusic.setEnabled(isMusicEnabled)
if (isMusicEnabled) {
gameMusic.start()
}
+ gameStartTime = System.currentTimeMillis()
+ piecesPlaced = 0
+ statsManager.startNewSession()
}
private fun restartGame() {
diff --git a/app/src/main/java/com/mintris/StatsActivity.kt b/app/src/main/java/com/mintris/StatsActivity.kt
new file mode 100644
index 0000000..269adb0
--- /dev/null
+++ b/app/src/main/java/com/mintris/StatsActivity.kt
@@ -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())
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/mintris/game/TitleScreen.kt b/app/src/main/java/com/mintris/game/TitleScreen.kt
index 379caaf..3a8978d 100644
--- a/app/src/main/java/com/mintris/game/TitleScreen.kt
+++ b/app/src/main/java/com/mintris/game/TitleScreen.kt
@@ -12,6 +12,10 @@ import java.util.Random
import android.util.Log
import com.mintris.model.HighScoreManager
import com.mintris.model.HighScore
+import kotlin.math.abs
+import androidx.core.graphics.withTranslation
+import androidx.core.graphics.withScale
+import androidx.core.graphics.withRotation
class TitleScreen @JvmOverloads constructor(
context: Context,
@@ -29,6 +33,14 @@ class TitleScreen @JvmOverloads constructor(
private var width = 0
private var height = 0
private val tetrominosToAdd = mutableListOf()
+ private val highScoreManager = HighScoreManager(context) // Pre-allocate HighScoreManager
+
+ // Touch handling variables
+ private var startX = 0f
+ private var startY = 0f
+ private var lastTouchX = 0f
+ private var lastTouchY = 0f
+ private val maxTapMovement = 20f // Maximum movement allowed for a tap (in pixels)
// Callback for when the user touches the screen
var onStartGame: (() -> Unit)? = null
@@ -187,58 +199,31 @@ class TitleScreen @JvmOverloads constructor(
tetrominosToAdd.add(createRandomTetromino())
} else {
try {
- // Save canvas state before rotation
- canvas.save()
-
- // Translate to the tetromino's position
- canvas.translate(tetromino.x, tetromino.y)
-
- // Scale according to the tetromino's scale factor
- canvas.scale(tetromino.scale, tetromino.scale)
-
- // Rotate around the center of the tetromino
- val centerX = tetromino.shape.size * cellSize / 2
- val centerY = tetromino.shape.size * cellSize / 2
- canvas.rotate(tetromino.rotation.toFloat(), centerX, centerY)
-
// Draw the tetromino
- for (row in tetromino.shape.indices) {
- for (col in 0 until tetromino.shape[row].size) {
- if (tetromino.shape[row][col] == 1) {
- // Draw larger glow effect
- glowPaint.alpha = 30
- canvas.drawRect(
- col * cellSize - 8,
- row * cellSize - 8,
- (col + 1) * cellSize + 8,
- (row + 1) * cellSize + 8,
- glowPaint
- )
+ for (y in 0 until tetromino.shape.size) {
+ for (x in 0 until tetromino.shape.size) {
+ if (tetromino.shape[y][x] == 1) {
+ val left = x * cellSize
+ val top = y * cellSize
+ val right = left + cellSize
+ val bottom = top + cellSize
- // Draw medium glow
- glowPaint.alpha = 60
- canvas.drawRect(
- col * cellSize - 4,
- row * cellSize - 4,
- (col + 1) * cellSize + 4,
- (row + 1) * cellSize + 4,
- glowPaint
- )
-
- // Draw main block
- canvas.drawRect(
- col * cellSize,
- row * cellSize,
- (col + 1) * cellSize,
- (row + 1) * cellSize,
- paint
- )
+ // Draw block with glow effect
+ canvas.withTranslation(tetromino.x, tetromino.y) {
+ withScale(tetromino.scale, tetromino.scale) {
+ withRotation(tetromino.rotation.toFloat(),
+ tetromino.shape.size * cellSize / 2,
+ tetromino.shape.size * cellSize / 2) {
+ // Draw glow
+ canvas.drawRect(left - 8f, top - 8f, right + 8f, bottom + 8f, glowPaint)
+ // Draw block
+ canvas.drawRect(left, top, right, bottom, paint)
+ }
+ }
+ }
}
}
}
-
- // Restore canvas state
- canvas.restore()
} catch (e: Exception) {
Log.e("TitleScreen", "Error drawing tetromino", e)
}
@@ -252,8 +237,7 @@ class TitleScreen @JvmOverloads constructor(
val titleY = height * 0.4f
canvas.drawText("mintris", width / 2f, titleY, titlePaint)
- // Draw high scores
- val highScoreManager = HighScoreManager(context)
+ // Draw high scores using pre-allocated manager
val highScores: List = highScoreManager.getHighScores()
val highScoreY = height * 0.5f
if (highScores.isNotEmpty()) {
@@ -274,10 +258,50 @@ class TitleScreen @JvmOverloads constructor(
}
override fun onTouchEvent(event: MotionEvent): Boolean {
- if (event.action == MotionEvent.ACTION_DOWN) {
- onStartGame?.invoke()
- return true
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ startX = event.x
+ startY = event.y
+ lastTouchX = event.x
+ lastTouchY = event.y
+ return true
+ }
+ MotionEvent.ACTION_MOVE -> {
+ val deltaX = event.x - lastTouchX
+ val deltaY = event.y - lastTouchY
+
+ // Update tetromino positions
+ for (tetromino in tetrominos) {
+ tetromino.x += deltaX * 0.5f
+ tetromino.y += deltaY * 0.5f
+ }
+
+ lastTouchX = event.x
+ lastTouchY = event.y
+ invalidate()
+ return true
+ }
+ MotionEvent.ACTION_UP -> {
+ val deltaX = event.x - startX
+ val deltaY = event.y - startY
+
+ // If the movement was minimal, treat as a tap
+ if (abs(deltaX) < maxTapMovement && abs(deltaY) < maxTapMovement) {
+ performClick()
+ }
+
+ return true
+ }
}
return super.onTouchEvent(event)
}
+
+ override fun performClick(): Boolean {
+ // Call the superclass's performClick
+ super.performClick()
+
+ // Handle the click event
+ onStartGame?.invoke()
+ return true
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/mintris/model/GameBoard.kt b/app/src/main/java/com/mintris/model/GameBoard.kt
index 3119971..42deaf3 100644
--- a/app/src/main/java/com/mintris/model/GameBoard.kt
+++ b/app/src/main/java/com/mintris/model/GameBoard.kt
@@ -31,6 +31,8 @@ class GameBoard(
var level = 1
var lines = 0
var isGameOver = false
+ var isHardDropInProgress = false // Make public
+ var isPieceLocking = false // Make public
// Scoring state
private var combo = 0
@@ -150,6 +152,9 @@ class GameBoard(
* Move the current piece down (soft drop)
*/
fun moveDown(): Boolean {
+ // Don't allow movement if a hard drop is in progress or piece is locking
+ if (isHardDropInProgress || isPieceLocking) return false
+
return if (canMove(0, 1)) {
currentPiece?.y = currentPiece?.y?.plus(1) ?: 0
onPieceMove?.invoke()
@@ -164,9 +169,19 @@ class GameBoard(
* Hard drop the current piece
*/
fun hardDrop() {
- while (moveDown()) {
- // Keep moving down until blocked
+ if (isHardDropInProgress || isPieceLocking) return // Prevent multiple hard drops
+
+ isHardDropInProgress = true
+ val piece = currentPiece ?: return
+
+ // Move piece down until it can't move anymore
+ while (canMove(0, 1)) {
+ piece.y++
+ onPieceMove?.invoke()
}
+
+ // Lock the piece immediately
+ lockPiece()
}
/**
@@ -249,9 +264,12 @@ class GameBoard(
}
/**
- * Lock the current piece in place and check for completed lines
+ * Lock the current piece in place
*/
- fun lockPiece() {
+ private fun lockPiece() {
+ if (isPieceLocking) return // Prevent recursive locking
+ isPieceLocking = true
+
val piece = currentPiece ?: return
// Add the piece to the grid
@@ -275,11 +293,15 @@ class GameBoard(
// Find and clear lines immediately
findAndClearLines()
- // Spawn new piece
+ // Spawn new piece immediately
spawnPiece()
// Allow holding piece again after locking
canHold = true
+
+ // Reset both states after everything is done
+ isPieceLocking = false
+ isHardDropInProgress = false
}
/**
diff --git a/app/src/main/java/com/mintris/model/StatsManager.kt b/app/src/main/java/com/mintris/model/StatsManager.kt
new file mode 100644
index 0000000..3a3eae7
--- /dev/null
+++ b/app/src/main/java/com/mintris/model/StatsManager.kt
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 7271d89..ab63a51 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -135,10 +135,92 @@
android:textStyle="bold" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -209,6 +291,16 @@
android:text="@string/high_scores"
android:textColor="@color/white"
android:textSize="18sp" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 871a7af..41c0fc2 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -20,4 +20,28 @@
New High Score!
Save
Back
+
+
+ Lifetime Stats
+ Best Performance
+ Total Games: %d
+ Total Score: %d
+ Total Lines: %d
+ Total Pieces: %d
+ Total Time: %s
+ Max Level: %d
+ Max Score: %d
+ Max Lines: %d
+ Stats
+ Session Stats
+ Score: %d
+ Lines: %d
+ Pieces: %d
+ Time: %s
+ Level: %d
+ Line Clears
+ Singles: %d
+ Doubles: %d
+ Triples: %d
+ Tetrises: %d
\ No newline at end of file