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" /> + +