diff --git a/README.md b/README.md index 2a67a0f..5bd8440 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# Mintris +# pixelmintdrop -A modern Tetris implementation for Android, featuring smooth animations, responsive controls, and a beautiful minimalist design. +A modern block-stacking puzzle game for Android, featuring smooth animations, responsive controls, and a beautiful minimalist design. ## Features ### Core Gameplay -- Classic Tetris mechanics +- Classic block-stacking mechanics - 7-bag randomizer for piece distribution - Ghost piece preview - Hard drop and soft drop @@ -29,7 +29,7 @@ The game features a comprehensive scoring system: - Single line: 40 points - Double: 100 points - Triple: 300 points -- Tetris (4 lines): 1200 points +- Quad (4 lines): 1200 points #### Multipliers @@ -48,15 +48,15 @@ The game features a comprehensive scoring system: - 4 combos: 2.5x - 5+ combos: 3.0x -3. **Back-to-Back Tetris** - - 50% bonus (1.5x) for consecutive Tetris clears - - Resets if a non-Tetris clear is performed +3. **Back-to-Back Quad** + - 50% bonus (1.5x) for consecutive quad clears + - Resets if a non-quad clear is performed 4. **Perfect Clear** - 2x for single line - 3x for double - 4x for triple - - 5x for Tetris + - 5x for quad - Awarded when clearing lines without leaving blocks 5. **All Clear** @@ -219,7 +219,7 @@ private fun onGameOver(score: Long) { 1. Clone the repository: ```bash -git clone https://github.com/cmclark00/mintris.git +git clone https://github.com/cmclark00/pixelmintdrop.git ``` 2. Open the project in Android Studio diff --git a/app/build.gradle b/app/build.gradle index 21d6285..45c83db 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,13 +4,13 @@ plugins { } android { - namespace 'com.mintris' - compileSdk 34 + namespace "com.pixelmintdrop" + compileSdk 35 defaultConfig { - applicationId "com.mintris" + applicationId "com.pixelmintdrop" minSdk 30 - targetSdk 34 + targetSdk 35 versionCode 1 versionName "1.0" @@ -27,6 +27,7 @@ android { buildFeatures { viewBinding true + dataBinding true } compileOptions { diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 403e50d..461f647 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -6,17 +6,17 @@ # http://developer.android.com/guide/developing/tools/proguard.html # Keep models intact --keep class com.mintris.model.** { *; } +-keep class com.pixelmintdrop.model.** { *; } # Keep game classes intact to prevent issues --keep class com.mintris.game.** { *; } +-keep class com.pixelmintdrop.game.** { *; } # Preserve critical classes that might be used through reflection --keep class com.mintris.audio.GameMusic { *; } --keep class com.mintris.ui.** { *; } +-keep class com.pixelmintdrop.audio.GameMusic { *; } +-keep class com.pixelmintdrop.ui.** { *; } # Keep all public methods in the MainActivity --keepclassmembers class com.mintris.MainActivity { +-keepclassmembers class com.pixelmintdrop.MainActivity { public *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3322422..0d57748 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,11 +12,11 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/Theme.Mintris"> + android:theme="@style/Theme.pixelmintdrop"> - val width = piece.getWidth() - val height = piece.getHeight() - - // Calculate block size for the preview (smaller than main board) - val previewBlockSize = min( - canvas.width.toFloat() / (width + 2), - canvas.height.toFloat() / (height + 2) - ) - - // Center the piece in the preview area - val previewLeft = (canvas.width - width * previewBlockSize) / 2 - val previewTop = (canvas.height - height * previewBlockSize) / 2 - - // Draw subtle background glow - val glowPaint = Paint().apply { - color = Color.WHITE - alpha = 10 - maskFilter = BlurMaskFilter(previewBlockSize * 0.5f, BlurMaskFilter.Blur.OUTER) - } - canvas.drawRect( - previewLeft - previewBlockSize, - previewTop - previewBlockSize, - previewLeft + width * previewBlockSize + previewBlockSize, - previewTop + height * previewBlockSize + previewBlockSize, - glowPaint - ) - - for (y in 0 until height) { - for (x in 0 until width) { - if (piece.isBlockAt(x, y)) { - val left = previewLeft + x * previewBlockSize - val top = previewTop + y * previewBlockSize - val right = left + previewBlockSize - val bottom = top + previewBlockSize - - // Draw block with subtle glow - val rect = RectF(left + 1, top + 1, right - 1, bottom - 1) - canvas.drawRect(rect, blockPaint) - - // Draw subtle border glow - val glowRect = RectF(left, top, right, bottom) - canvas.drawRect(glowRect, glowPaint) - } - } - } - } - } - } -} \ 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 deleted file mode 100644 index 61bcf11..0000000 --- a/app/src/main/java/com/mintris/model/GameBoard.kt +++ /dev/null @@ -1,765 +0,0 @@ -package com.mintris.model - -import android.util.Log - -/** - * Represents the game board (grid) and manages game state - */ -class GameBoard( - val width: Int = 10, - val height: Int = 20 -) { - companion object { - private const val TAG = "GameBoard" - } - - // Board grid to track locked pieces - // True = occupied, False = empty - private val grid = Array(height) { BooleanArray(width) { false } } - - // Current active tetromino - private var currentPiece: Tetromino? = null - - // Next tetromino to be played - private var nextPiece: Tetromino? = null - - // Hold piece - private var holdPiece: Tetromino? = null - private var canHold = true - - // 7-bag randomizer - private val bag = mutableListOf() - - // Game state - var score = 0 - var level = 1 - var startingLevel = 1 // Add this line to track the starting level - var lines = 0 - var isGameOver = false - var isHardDropInProgress = false // Make public - var isPieceLocking = false // Make public - private var isPlayerSoftDrop = false // Track if the drop is player-initiated - private var lastLevel = 1 // Add this to track the previous level - - // Scoring state - private var combo = 0 - private var lastClearWasTetris = false - private var lastClearWasPerfect = false - private var lastClearWasAllClear = false - private var lastPieceClearedLines = false // Track if the last piece placed cleared lines - - // Animation state - var linesToClear = mutableListOf() - var isLineClearAnimationInProgress = false - - // Initial game speed (milliseconds per drop) - var dropInterval = 1000L - - // Callbacks for game events - var onPieceMove: (() -> Unit)? = null - var onPieceLock: (() -> Unit)? = null - var onNextPieceChanged: (() -> Unit)? = null - var onLineClear: ((Int, List) -> Unit)? = null - var onPiecePlaced: (() -> Unit)? = null // New callback for when a piece is placed - - // Store the last cleared lines - private val lastClearedLines = mutableListOf() - - // Add spawn protection variables - private var pieceSpawnTime = 0L - private val spawnGracePeriod = 250L // Changed from 150ms to 250ms - - init { - spawnNextPiece() - spawnPiece() - } - - /** - * Generates the next tetromino piece using 7-bag randomizer - */ - private fun spawnNextPiece() { - // If bag is empty, refill it with all piece types - if (bag.isEmpty()) { - bag.addAll(TetrominoType.entries.toTypedArray()) - bag.shuffle() - } - - // Take the next piece from the bag - nextPiece = Tetromino(bag.removeAt(0)) - onNextPieceChanged?.invoke() - } - - /** - * Hold the current piece - */ - fun holdPiece() { - if (!canHold) return - - val current = currentPiece - if (holdPiece == null) { - // If no piece is held, hold current piece and spawn new one - holdPiece = current - currentPiece = nextPiece - spawnNextPiece() - // Reset position of new piece - currentPiece?.apply { - x = (width - getWidth()) / 2 - y = 0 - } - } else { - // Swap current piece with held piece - currentPiece = holdPiece - holdPiece = current - // Reset position of swapped piece - currentPiece?.apply { - x = (width - getWidth()) / 2 - y = 0 - } - } - canHold = false - } - - /** - * Get the currently held piece - */ - fun getHoldPiece(): Tetromino? = holdPiece - - /** - * Get the next piece that will be spawned - */ - fun getNextPiece(): Tetromino? = nextPiece - - /** - * Spawns the current tetromino at the top of the board - */ - fun spawnPiece() { - Log.d(TAG, "spawnPiece() started - current states: isHardDropInProgress=$isHardDropInProgress, isPieceLocking=$isPieceLocking") - - currentPiece = nextPiece - spawnNextPiece() - - // Center the piece horizontally and spawn one unit higher - currentPiece?.apply { - x = (width - getWidth()) / 2 - y = -1 // Spawn one unit above the top of the screen - - Log.d(TAG, "spawnPiece() - new piece spawned at position (${x},${y}), type=${type}") - - // Set the spawn time for the grace period - pieceSpawnTime = System.currentTimeMillis() - - // Check if the piece can be placed (Game Over condition) - if (!canMove(0, 0)) { - isGameOver = true - Log.d(TAG, "spawnPiece() - Game Over condition detected") - } - } - } - - /** - * Move the current piece left - */ - fun moveLeft() { - if (canMove(-1, 0)) { - currentPiece?.x = currentPiece?.x?.minus(1) ?: 0 - onPieceMove?.invoke() - } - } - - /** - * Move the current piece right - */ - fun moveRight() { - if (canMove(1, 0)) { - currentPiece?.x = currentPiece?.x?.plus(1) ?: 0 - onPieceMove?.invoke() - } - } - - /** - * 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 - // Only add soft drop points if it's a player-initiated drop - if (isPlayerSoftDrop) { - score += 1 - } - onPieceMove?.invoke() - true - } else { - // Check if we're within the spawn grace period - val currentTime = System.currentTimeMillis() - if (currentTime - pieceSpawnTime < spawnGracePeriod) { - Log.d(TAG, "moveDown() - not locking piece due to spawn grace period (${currentTime - pieceSpawnTime}ms < ${spawnGracePeriod}ms)") - return false - } - - lockPiece() - false - } - } - - /** - * Player-initiated soft drop - */ - fun softDrop() { - isPlayerSoftDrop = true - moveDown() - isPlayerSoftDrop = false - } - - /** - * Hard drop the current piece - */ - fun hardDrop() { - if (isHardDropInProgress || isPieceLocking) { - Log.d(TAG, "hardDrop() called but blocked: isHardDropInProgress=$isHardDropInProgress, isPieceLocking=$isPieceLocking") - return // Prevent multiple hard drops - } - - // Check if we're within the spawn grace period - val currentTime = System.currentTimeMillis() - if (currentTime - pieceSpawnTime < spawnGracePeriod) { - Log.d(TAG, "hardDrop() - blocked due to spawn grace period (${currentTime - pieceSpawnTime}ms < ${spawnGracePeriod}ms)") - return - } - - Log.d(TAG, "hardDrop() started - setting isHardDropInProgress=true") - isHardDropInProgress = true - val piece = currentPiece ?: return - - // Count how many cells the piece will drop - var dropDistance = 0 - while (canMove(0, dropDistance + 1)) { - dropDistance++ - } - - Log.d(TAG, "hardDrop() - piece will drop $dropDistance cells, position before: (${piece.x},${piece.y})") - - // Move piece down until it can't move anymore - while (canMove(0, 1)) { - piece.y++ - onPieceMove?.invoke() - } - - Log.d(TAG, "hardDrop() - piece final position: (${piece.x},${piece.y})") - - // Add hard drop points (2 points per cell) - score += dropDistance * 2 - - // Lock the piece immediately - lockPiece() - } - - /** - * Rotate the current piece clockwise - */ - fun rotate() { - currentPiece?.let { - // Save current rotation - val originalX = it.x - val originalY = it.y - - // Try to rotate - it.rotateClockwise() - - // Wall kick logic - try to move the piece if rotation causes collision - if (!canMove(0, 0)) { - // Try to move left - if (canMove(-1, 0)) { - it.x-- - } - // Try to move right - else if (canMove(1, 0)) { - it.x++ - } - // Try to move 2 spaces (for I piece) - else if (canMove(-2, 0)) { - it.x -= 2 - } - else if (canMove(2, 0)) { - it.x += 2 - } - // Try to move up for floor kicks - else if (canMove(0, -1)) { - it.y-- - } - // Revert if can't find a valid position - else { - it.rotateCounterClockwise() - it.x = originalX - it.y = originalY - } - } - } - } - - /** - * Rotate the current piece counterclockwise - */ - fun rotateCounterClockwise() { - currentPiece?.let { - // Save current rotation - val originalX = it.x - val originalY = it.y - - // Try to rotate - it.rotateCounterClockwise() - - // Wall kick logic - try to move the piece if rotation causes collision - if (!canMove(0, 0)) { - // Try to move left - if (canMove(-1, 0)) { - it.x-- - } - // Try to move right - else if (canMove(1, 0)) { - it.x++ - } - // Try to move 2 spaces (for I piece) - else if (canMove(-2, 0)) { - it.x -= 2 - } - else if (canMove(2, 0)) { - it.x += 2 - } - // Try to move up for floor kicks - else if (canMove(0, -1)) { - it.y-- - } - // Revert if can't find a valid position - else { - it.rotateClockwise() - it.x = originalX - it.y = originalY - } - } - } - } - - /** - * Check if the current piece can move to the given position - */ - fun canMove(deltaX: Int, deltaY: Int): Boolean { - val piece = currentPiece ?: return false - - val newX = piece.x + deltaX - val newY = piece.y + deltaY - - for (y in 0 until piece.getHeight()) { - for (x in 0 until piece.getWidth()) { - if (piece.isBlockAt(x, y)) { - val boardX = newX + x - val boardY = newY + y - - // Check if the position is outside the board horizontally - if (boardX < 0 || boardX >= width) { - return false - } - - // Check if the position is below the board - if (boardY >= height) { - return false - } - - // Check if the position is already occupied (but not if it's above the board) - if (boardY >= 0 && grid[boardY][boardX]) { - return false - } - - // Check if the position is more than one unit above the top of the screen - if (boardY < -1) { - return false - } - } - } - } - - return true - } - - /** - * Lock the current piece in place - */ - private fun lockPiece() { - if (isPieceLocking) { - Log.d(TAG, "lockPiece() called but blocked: isPieceLocking=$isPieceLocking") - return // Prevent recursive locking - } - - Log.d(TAG, "lockPiece() started - setting isPieceLocking=true, current isHardDropInProgress=$isHardDropInProgress") - isPieceLocking = true - - val piece = currentPiece ?: return - - // Add the piece to the grid - for (y in 0 until piece.getHeight()) { - for (x in 0 until piece.getWidth()) { - if (piece.isBlockAt(x, y)) { - val boardX = piece.x + x - val boardY = piece.y + y - - // Only add to grid if within bounds - if (boardY >= 0 && boardY < height && boardX >= 0 && boardX < width) { - grid[boardY][boardX] = true - } - } - } - } - - // Trigger the piece lock vibration - onPieceLock?.invoke() - - // Notify that a piece was placed - onPiecePlaced?.invoke() - - // Find and clear lines immediately - findAndClearLines() - - // IMPORTANT: Reset the hard drop flag before spawning a new piece - // This prevents the immediate hard drop of the next piece - if (isHardDropInProgress) { - Log.d(TAG, "lockPiece() - resetting isHardDropInProgress=false BEFORE spawning new piece") - isHardDropInProgress = false - } - - // Log piece position before spawning new piece - Log.d(TAG, "lockPiece() - about to spawn new piece at y=${piece.y}, isHardDropInProgress=$isHardDropInProgress") - - // Spawn new piece immediately - spawnPiece() - - // Allow holding piece again after locking - canHold = true - - // Reset locking state - isPieceLocking = false - Log.d(TAG, "lockPiece() completed - reset flags: isPieceLocking=false, isHardDropInProgress=$isHardDropInProgress") - } - - /** - * Find and clear completed lines immediately - */ - private fun findAndClearLines() { - // Quick scan for completed lines - var shiftAmount = 0 - var y = height - 1 - val linesToClear = mutableListOf() - - while (y >= 0) { - if (grid[y].all { it }) { - // Line is full, add to lines to clear - linesToClear.add(y) - shiftAmount++ - } else if (shiftAmount > 0) { - // Shift this row down by shiftAmount - System.arraycopy(grid[y], 0, grid[y + shiftAmount], 0, width) - } - y-- - } - - // Store the last cleared lines - lastClearedLines.clear() - lastClearedLines.addAll(linesToClear) - - // If lines were cleared, calculate score in background and trigger callback - if (shiftAmount > 0) { - // Log line clear - Log.d(TAG, "Lines cleared: $shiftAmount") - - // Trigger line clear callback on main thread with the lines that were cleared - val mainHandler = android.os.Handler(android.os.Looper.getMainLooper()) - mainHandler.post { - // Call the line clear callback with the cleared line count - try { - Log.d(TAG, "Triggering onLineClear callback with $shiftAmount lines") - val clearedLines = getLastClearedLines() - onLineClear?.invoke(shiftAmount, clearedLines) - Log.d(TAG, "onLineClear callback completed successfully") - } catch (e: Exception) { - Log.e(TAG, "Error in onLineClear callback", e) - } - } - - // Clear top rows after callback - for (y in 0 until shiftAmount) { - java.util.Arrays.fill(grid[y], false) - } - - Thread { - calculateScore(shiftAmount) - }.start() - } - - // Update combo based on whether this piece cleared lines - if (shiftAmount > 0) { - if (lastPieceClearedLines) { - combo++ - } else { - combo = 1 // Start new combo - } - } else { - combo = 0 // Reset combo if no lines cleared - } - lastPieceClearedLines = shiftAmount > 0 - } - - /** - * Calculate score for cleared lines - */ - private fun calculateScore(clearedLines: Int) { - // Pre-calculated score multipliers for better performance - val baseScore = when (clearedLines) { - 1 -> 40 - 2 -> 100 - 3 -> 300 - 4 -> 1200 - else -> 0 - } - - // Check for perfect clear (no blocks left) - val isPerfectClear = !grid.any { row -> row.any { it } } - - // Check for all clear (no blocks in playfield) - val isAllClear = !grid.any { row -> row.any { it } } && - currentPiece == null && - nextPiece == null - - // Calculate combo multiplier - val comboMultiplier = if (combo > 0) { - when (combo) { - 1 -> 1.0 - 2 -> 1.5 - 3 -> 2.0 - 4 -> 2.5 - else -> 3.0 - } - } else 1.0 - - // Calculate back-to-back Tetris bonus - val backToBackMultiplier = if (clearedLines == 4 && lastClearWasTetris) 1.5 else 1.0 - - // Calculate perfect clear bonus - val perfectClearMultiplier = if (isPerfectClear) { - when (clearedLines) { - 1 -> 2.0 - 2 -> 3.0 - 3 -> 4.0 - 4 -> 5.0 - else -> 1.0 - } - } else 1.0 - - // Calculate all clear bonus - val allClearMultiplier = if (isAllClear) 2.0 else 1.0 - - // Calculate T-Spin bonus - val tSpinMultiplier = if (isTSpin()) { - when (clearedLines) { - 1 -> 2.0 - 2 -> 4.0 - 3 -> 6.0 - else -> 1.0 - } - } else 1.0 - - // Calculate final score with all multipliers - val finalScore = (baseScore * level * comboMultiplier * - backToBackMultiplier * perfectClearMultiplier * - allClearMultiplier * tSpinMultiplier).toInt() - - // Update score on main thread - Thread { - score += finalScore - }.start() - - // Update line clear state - lastClearWasTetris = clearedLines == 4 - lastClearWasPerfect = isPerfectClear - lastClearWasAllClear = isAllClear - - // Update lines cleared and level - lines += clearedLines - // Calculate level based on lines cleared, but ensure it's never below the starting level - level = Math.max((lines / 10) + 1, startingLevel) - - // Update game speed based on level (NES formula) - dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong() - } - - /** - * Check if the last move was a T-Spin - */ - private fun isTSpin(): Boolean { - val piece = currentPiece ?: return false - if (piece.type != TetrominoType.T) return false - - // Count occupied corners around the T piece - var occupiedCorners = 0 - val centerX = piece.x + 1 - val centerY = piece.y + 1 - - // Check all four corners - if (isOccupied(centerX - 1, centerY - 1)) occupiedCorners++ - if (isOccupied(centerX + 1, centerY - 1)) occupiedCorners++ - if (isOccupied(centerX - 1, centerY + 1)) occupiedCorners++ - if (isOccupied(centerX + 1, centerY + 1)) occupiedCorners++ - - // T-Spin requires at least 3 occupied corners - return occupiedCorners >= 3 - } - - /** - * Get the ghost piece position (preview of where piece will land) - */ - fun getGhostY(): Int { - val piece = currentPiece ?: return 0 - var ghostY = piece.y - - // Find how far the piece can move down - while (true) { - if (canMove(0, ghostY - piece.y + 1)) { - ghostY++ - } else { - break - } - } - - // Ensure ghostY doesn't exceed the board height - return ghostY.coerceAtMost(height - 1) - } - - /** - * Get the current tetromino - */ - fun getCurrentPiece(): Tetromino? = currentPiece - - /** - * Check if a cell in the grid is occupied - */ - fun isOccupied(x: Int, y: Int): Boolean { - return if (x in 0 until width && y in 0 until height) { - grid[y][x] - } else { - false - } - } - - /** - * Check if a line is completely filled - */ - fun isLineFull(y: Int): Boolean { - return if (y in 0 until height) { - grid[y].all { it } - } else { - false - } - } - - /** - * Update the current level and adjust game parameters - */ - fun updateLevel(newLevel: Int) { - lastLevel = level - level = newLevel.coerceIn(1, 20) - startingLevel = level // Store the starting level - dropInterval = getDropIntervalForLevel(level) - } - - /** - * Start a new game - */ - fun startGame() { - reset() - // Initialize pieces - spawnNextPiece() - spawnPiece() - } - - /** - * Reset the game board - */ - fun reset() { - // Clear the grid - for (y in 0 until height) { - for (x in 0 until width) { - grid[y][x] = false - } - } - - // Reset game state - score = 0 - level = startingLevel // Use starting level instead of resetting to 1 - lastLevel = level // Reset lastLevel to match the current level - lines = 0 - isGameOver = false - dropInterval = getDropIntervalForLevel(level) // Use helper method - - // Reset scoring state - combo = 0 - lastClearWasTetris = false - lastClearWasPerfect = false - lastClearWasAllClear = false - lastPieceClearedLines = false - - // Reset piece state - holdPiece = null - canHold = true - bag.clear() - - // Clear current and next pieces - currentPiece = null - nextPiece = null - } - - /** - * Clear completed lines and move blocks down (legacy method, kept for reference) - */ - private fun clearLines(): Int { - return linesToClear.size // Return the number of lines that will be cleared - } - - /** - * Get the current combo count - */ - fun getCombo(): Int = combo - - /** - * Get the list of lines that were most recently cleared - */ - private fun getLastClearedLines(): List { - return lastClearedLines.toList() - } - - /** - * Get the last level - */ - fun getLastLevel(): Int = lastLevel - - /** - * Update the game state (called by game loop) - */ - fun update() { - if (!isGameOver) { - moveDown() - } - } - - /** - * Get the drop interval for the given level - */ - private fun getDropIntervalForLevel(level: Int): Long { - val cappedLevel = level.coerceIn(1, 20) - // Update game speed based on level (NES formula) - return (1000 * Math.pow(0.8, (cappedLevel - 1).toDouble())).toLong() - } - - /** - * Update the game level - */ -} \ No newline at end of file diff --git a/app/src/main/java/com/mintris/model/StatsManager.kt b/app/src/main/java/com/mintris/model/StatsManager.kt deleted file mode 100644 index cc5272f..0000000 --- a/app/src/main/java/com/mintris/model/StatsManager.kt +++ /dev/null @@ -1,196 +0,0 @@ -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 - - fun resetStats() { - // Reset all lifetime stats - totalGames = 0 - totalScore = 0 - totalLines = 0 - totalPieces = 0 - totalTime = 0 - maxLevel = 0 - maxScore = 0 - maxLines = 0 - - // Reset line clear stats - totalSingles = 0 - totalDoubles = 0 - totalTriples = 0 - totalTetrises = 0 - - // Save the reset stats - saveStats() - } - - 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/java/com/mintris/HighScoreEntryActivity.kt b/app/src/main/java/com/pixelmintdrop/HighScoreEntryActivity.kt similarity index 93% rename from app/src/main/java/com/mintris/HighScoreEntryActivity.kt rename to app/src/main/java/com/pixelmintdrop/HighScoreEntryActivity.kt index 339c3b6..09e2064 100644 --- a/app/src/main/java/com/mintris/HighScoreEntryActivity.kt +++ b/app/src/main/java/com/pixelmintdrop/HighScoreEntryActivity.kt @@ -1,16 +1,12 @@ -package com.mintris +package com.pixelmintdrop import android.app.Activity import android.os.Bundle -import android.widget.Button -import android.widget.EditText -import android.widget.TextView -import android.view.View import androidx.appcompat.app.AppCompatActivity -import com.mintris.databinding.HighScoreEntryBinding -import com.mintris.model.HighScore -import com.mintris.model.HighScoreManager -import com.mintris.model.PlayerProgressionManager +import com.pixelmintdrop.databinding.HighScoreEntryBinding +import com.pixelmintdrop.model.HighScore +import com.pixelmintdrop.model.HighScoreManager +import com.pixelmintdrop.model.PlayerProgressionManager import android.graphics.Color import android.view.KeyEvent import android.view.InputDevice diff --git a/app/src/main/java/com/mintris/HighScoresActivity.kt b/app/src/main/java/com/pixelmintdrop/HighScoresActivity.kt similarity index 94% rename from app/src/main/java/com/mintris/HighScoresActivity.kt rename to app/src/main/java/com/pixelmintdrop/HighScoresActivity.kt index 0fdc75d..ea21392 100644 --- a/app/src/main/java/com/mintris/HighScoresActivity.kt +++ b/app/src/main/java/com/pixelmintdrop/HighScoresActivity.kt @@ -1,15 +1,12 @@ -package com.mintris +package com.pixelmintdrop import android.os.Bundle -import android.widget.Button -import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.mintris.databinding.HighScoresBinding -import com.mintris.model.HighScoreAdapter -import com.mintris.model.HighScoreManager -import com.mintris.model.PlayerProgressionManager +import com.pixelmintdrop.databinding.HighScoresBinding +import com.pixelmintdrop.model.HighScoreAdapter +import com.pixelmintdrop.model.HighScoreManager +import com.pixelmintdrop.model.PlayerProgressionManager import android.graphics.Color import android.util.Log import android.view.KeyEvent diff --git a/app/src/main/java/com/mintris/MainActivity.kt b/app/src/main/java/com/pixelmintdrop/MainActivity.kt similarity index 97% rename from app/src/main/java/com/mintris/MainActivity.kt rename to app/src/main/java/com/pixelmintdrop/MainActivity.kt index 22f2ea8..9384137 100644 --- a/app/src/main/java/com/mintris/MainActivity.kt +++ b/app/src/main/java/com/pixelmintdrop/MainActivity.kt @@ -1,4 +1,4 @@ -package com.mintris +package com.pixelmintdrop import android.content.Context import android.content.Intent @@ -9,18 +9,18 @@ import android.os.Vibrator import android.view.View import android.view.HapticFeedbackConstants import androidx.appcompat.app.AppCompatActivity -import com.mintris.databinding.ActivityMainBinding -import com.mintris.game.GameHaptics -import com.mintris.game.GameView -import com.mintris.game.TitleScreen -import com.mintris.model.GameBoard -import com.mintris.audio.GameMusic -import com.mintris.model.HighScoreManager -import com.mintris.model.PlayerProgressionManager -import com.mintris.model.StatsManager -import com.mintris.ui.ProgressionScreen -import com.mintris.ui.ThemeSelector -import com.mintris.ui.BlockSkinSelector +import com.pixelmintdrop.databinding.ActivityMainBinding +import com.pixelmintdrop.game.GameHaptics +import com.pixelmintdrop.game.GameView +import com.pixelmintdrop.game.TitleScreen +import com.pixelmintdrop.model.GameBoard +import com.pixelmintdrop.audio.GameMusic +import com.pixelmintdrop.model.HighScoreManager +import com.pixelmintdrop.model.PlayerProgressionManager +import com.pixelmintdrop.model.StatsManager +import com.pixelmintdrop.ui.ProgressionScreen +import com.pixelmintdrop.ui.ThemeSelector +import com.pixelmintdrop.ui.BlockSkinSelector import java.text.SimpleDateFormat import java.util.* import android.graphics.Color @@ -32,12 +32,11 @@ import android.view.KeyEvent import android.os.Handler import android.os.Looper import android.view.MotionEvent -import com.mintris.game.GamepadController +import com.pixelmintdrop.game.GamepadController import android.view.InputDevice import android.widget.Toast import android.content.BroadcastReceiver import android.content.IntentFilter -import android.app.AlertDialog import android.graphics.drawable.ColorDrawable import androidx.core.content.ContextCompat import android.widget.Button @@ -153,7 +152,7 @@ class MainActivity : AppCompatActivity(), pauseMenuScrollView = binding.pauseMenuScrollView // Load random mode setting - isRandomModeEnabled = getSharedPreferences("com.mintris.preferences", Context.MODE_PRIVATE) + isRandomModeEnabled = getSharedPreferences("com.com.pixelmintgames.pixelmintdrop.preferences", Context.MODE_PRIVATE) .getBoolean("random_mode_enabled", false) // Initialize gamepad controller @@ -302,7 +301,14 @@ class MainActivity : AppCompatActivity(), statsManager.updateSessionStats(finalScore, gameBoard.lines, piecesPlaced, timePlayedMs, currentLevel) // Handle progression - XP earned, potential level up - val xpGained = progressionManager.calculateGameXP(finalScore, gameBoard.lines, currentLevel, timePlayedMs, statsManager.getSessionTetrises(), 0) + val xpGained = progressionManager.calculateGameXP( + score = finalScore, + lines = gameBoard.lines, + level = currentLevel, + timePlayedMs = timePlayedMs, + quadCount = statsManager.getSessionQuads(), + perfectClearCount = statsManager.getSessionPerfectClears() + ) val newRewards = progressionManager.addXP(xpGained) // Show progression screen if player earned XP @@ -472,9 +478,9 @@ class MainActivity : AppCompatActivity(), score = score, lines = gameBoard.lines, level = currentLevel, - gameTime = gameTime, - tetrisCount = statsManager.getSessionTetrises(), - perfectClearCount = 0 // Implement perfect clear tracking if needed + timePlayedMs = gameTime, + quadCount = statsManager.getSessionQuads(), + perfectClearCount = statsManager.getSessionPerfectClears() ) // Add XP and check for rewards @@ -497,7 +503,7 @@ class MainActivity : AppCompatActivity(), 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()) + binding.sessionQuadsText.text = getString(R.string.quads, statsManager.getSessionQuads()) // Flag to track if high score screen will be shown var showingHighScore = false @@ -624,7 +630,7 @@ class MainActivity : AppCompatActivity(), isEnabled = progressionManager.getPlayerLevel() >= 5 setOnCheckedChangeListener { _, isChecked -> isRandomModeEnabled = isChecked - getSharedPreferences("com.mintris.preferences", Context.MODE_PRIVATE) + getSharedPreferences("com.com.pixelmintgames.pixelmintdrop.preferences", Context.MODE_PRIVATE) .edit() .putBoolean("random_mode_enabled", isChecked) .apply() @@ -912,7 +918,14 @@ class MainActivity : AppCompatActivity(), statsManager.updateSessionStats(finalScore, gameBoard.lines, piecesPlaced, timePlayedMs, currentLevel) // Handle progression - XP earned, potential level up - val xpGained = progressionManager.calculateGameXP(finalScore, gameBoard.lines, currentLevel, timePlayedMs, statsManager.getSessionTetrises(), 0) + val xpGained = progressionManager.calculateGameXP( + score = finalScore, + lines = gameBoard.lines, + level = currentLevel, + timePlayedMs = timePlayedMs, + quadCount = statsManager.getSessionQuads(), + perfectClearCount = statsManager.getSessionPerfectClears() + ) val newRewards = progressionManager.addXP(xpGained) // Show progression screen if player earned XP @@ -1211,7 +1224,7 @@ class MainActivity : AppCompatActivity(), * Check if user has seen the gamepad help */ private fun hasSeenGamepadHelp(): Boolean { - val prefs = getSharedPreferences("com.mintris.preferences", Context.MODE_PRIVATE) + val prefs = getSharedPreferences("com.com.pixelmintgames.pixelmintdrop.preferences", Context.MODE_PRIVATE) return prefs.getBoolean("has_seen_gamepad_help", false) } @@ -1219,7 +1232,7 @@ class MainActivity : AppCompatActivity(), * Mark that user has seen the gamepad help */ private fun markGamepadHelpSeen() { - val prefs = getSharedPreferences("com.mintris.preferences", Context.MODE_PRIVATE) + val prefs = getSharedPreferences("com.com.pixelmintgames.pixelmintdrop.preferences", Context.MODE_PRIVATE) prefs.edit().putBoolean("has_seen_gamepad_help", true).apply() } diff --git a/app/src/main/java/com/mintris/StatsActivity.kt b/app/src/main/java/com/pixelmintdrop/StatsActivity.kt similarity index 93% rename from app/src/main/java/com/mintris/StatsActivity.kt rename to app/src/main/java/com/pixelmintdrop/StatsActivity.kt index c7d2e0e..1652365 100644 --- a/app/src/main/java/com/mintris/StatsActivity.kt +++ b/app/src/main/java/com/pixelmintdrop/StatsActivity.kt @@ -1,13 +1,11 @@ -package com.mintris +package com.pixelmintdrop import android.os.Bundle -import android.widget.Button -import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import com.mintris.databinding.ActivityStatsBinding -import com.mintris.model.StatsManager -import com.mintris.model.PlayerProgressionManager +import com.pixelmintdrop.databinding.ActivityStatsBinding +import com.pixelmintdrop.model.StatsManager +import com.pixelmintdrop.model.PlayerProgressionManager import android.graphics.Color import java.text.SimpleDateFormat import java.util.* @@ -80,7 +78,7 @@ class StatsActivity : AppCompatActivity() { binding.totalSinglesText.setTextColor(textColor) binding.totalDoublesText.setTextColor(textColor) binding.totalTriplesText.setTextColor(textColor) - binding.totalTetrisesText.setTextColor(textColor) + binding.totalQuadsText.setTextColor(textColor) binding.maxLevelText.setTextColor(textColor) binding.maxScoreText.setTextColor(textColor) binding.maxLinesText.setTextColor(textColor) @@ -118,7 +116,7 @@ class StatsActivity : AppCompatActivity() { 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()) + binding.totalQuadsText.text = getString(R.string.quads, statsManager.getTotalQuads()) // Update best performance stats binding.maxLevelText.text = getString(R.string.max_level, statsManager.getMaxLevel()) diff --git a/app/src/main/java/com/mintris/ThemeManager.kt b/app/src/main/java/com/pixelmintdrop/ThemeManager.kt similarity index 92% rename from app/src/main/java/com/mintris/ThemeManager.kt rename to app/src/main/java/com/pixelmintdrop/ThemeManager.kt index 56ce224..b2d1e1f 100644 --- a/app/src/main/java/com/mintris/ThemeManager.kt +++ b/app/src/main/java/com/pixelmintdrop/ThemeManager.kt @@ -1,4 +1,4 @@ -package com.mintris +package com.pixelmintdrop import android.graphics.Color diff --git a/app/src/main/java/com/mintris/accessibility/GameAccessibilityHelper.kt b/app/src/main/java/com/pixelmintdrop/accessibility/GameAccessibilityHelper.kt similarity index 82% rename from app/src/main/java/com/mintris/accessibility/GameAccessibilityHelper.kt rename to app/src/main/java/com/pixelmintdrop/accessibility/GameAccessibilityHelper.kt index 450e47f..45281c0 100644 --- a/app/src/main/java/com/mintris/accessibility/GameAccessibilityHelper.kt +++ b/app/src/main/java/com/pixelmintdrop/accessibility/GameAccessibilityHelper.kt @@ -1,4 +1,4 @@ -package com.mintris.accessibility +package com.pixelmintdrop.accessibility import android.content.Context import android.os.Build @@ -8,9 +8,9 @@ import android.view.accessibility.AccessibilityManager import android.widget.ImageButton import android.widget.TextView import androidx.core.view.ViewCompat -import com.mintris.R -import com.mintris.game.GameView -import com.mintris.model.TetrominoType +import com.pixelmintdrop.R +import com.pixelmintdrop.game.GameView +import com.pixelmintdrop.model.GamePieceType /** * Helper class to improve the game's accessibility for users with visual impairments @@ -111,13 +111,13 @@ class GameAccessibilityHelper(private val context: Context) { val pieceType = gameView.getCurrentPieceType() ?: return "No piece" return when (pieceType) { - TetrominoType.I -> "I piece, long bar" - TetrominoType.J -> "J piece, hook shape pointing left" - TetrominoType.L -> "L piece, hook shape pointing right" - TetrominoType.O -> "O piece, square shape" - TetrominoType.S -> "S piece, zigzag shape" - TetrominoType.T -> "T piece, T shape" - TetrominoType.Z -> "Z piece, reverse zigzag shape" + GamePieceType.I -> "I piece, long bar" + GamePieceType.J -> "J piece, hook shape pointing left" + GamePieceType.L -> "L piece, hook shape pointing right" + GamePieceType.O -> "O piece, square shape" + GamePieceType.S -> "S piece, zigzag shape" + GamePieceType.T -> "T piece, T shape" + GamePieceType.Z -> "Z piece, reverse zigzag shape" } } @@ -134,4 +134,19 @@ class GameAccessibilityHelper(private val context: Context) { fun announceLevelUp(view: View, newLevel: Int) { announceGameEvent(view, "Level up! Now at level $newLevel") } + + /** + * Get the piece name for accessibility announcements + */ + private fun getPieceName(type: GamePieceType): String { + return when (type) { + GamePieceType.I -> "I piece" + GamePieceType.J -> "J piece" + GamePieceType.L -> "L piece" + GamePieceType.O -> "O piece" + GamePieceType.S -> "S piece" + GamePieceType.T -> "T piece" + GamePieceType.Z -> "Z piece" + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/mintris/audio/GameMusic.kt b/app/src/main/java/com/pixelmintdrop/audio/GameMusic.kt similarity index 98% rename from app/src/main/java/com/mintris/audio/GameMusic.kt rename to app/src/main/java/com/pixelmintdrop/audio/GameMusic.kt index cca3244..38a8615 100644 --- a/app/src/main/java/com/mintris/audio/GameMusic.kt +++ b/app/src/main/java/com/pixelmintdrop/audio/GameMusic.kt @@ -1,11 +1,11 @@ -package com.mintris.audio +package com.pixelmintdrop.audio import android.content.Context import android.media.MediaPlayer import android.media.AudioAttributes import android.os.Build import android.util.Log -import com.mintris.R +import com.pixelmintdrop.R class GameMusic(private val context: Context) { private var mediaPlayer: MediaPlayer? = null diff --git a/app/src/main/java/com/mintris/game/GameHaptics.kt b/app/src/main/java/com/pixelmintdrop/game/GameHaptics.kt similarity index 97% rename from app/src/main/java/com/mintris/game/GameHaptics.kt rename to app/src/main/java/com/pixelmintdrop/game/GameHaptics.kt index 731ac8b..7629065 100644 --- a/app/src/main/java/com/mintris/game/GameHaptics.kt +++ b/app/src/main/java/com/pixelmintdrop/game/GameHaptics.kt @@ -1,4 +1,4 @@ -package com.mintris.game +package com.pixelmintdrop.game import android.content.Context import android.os.Build @@ -53,7 +53,7 @@ class GameHaptics(private val context: Context) { 1 -> (50L * multiplier).toLong() // Single line: short vibration 2 -> (80L * multiplier).toLong() // Double line: slightly longer 3 -> (120L * multiplier).toLong() // Triple line: even longer - 4 -> (200L * multiplier).toLong() // Tetris: longest vibration + 4 -> (200L * multiplier).toLong() // Quad: longest vibration else -> (50L * multiplier).toLong() } @@ -61,7 +61,7 @@ class GameHaptics(private val context: Context) { 1 -> (80 * multiplier).toInt().coerceAtMost(255) // Single line: mild vibration 2 -> (120 * multiplier).toInt().coerceAtMost(255) // Double line: medium vibration 3 -> (180 * multiplier).toInt().coerceAtMost(255) // Triple line: strong vibration - 4 -> 255 // Tetris: maximum vibration + 4 -> 255 // Quad: maximum vibration else -> (80 * multiplier).toInt().coerceAtMost(255) } diff --git a/app/src/main/java/com/mintris/game/GameLifecycleManager.kt b/app/src/main/java/com/pixelmintdrop/game/GameLifecycleManager.kt similarity index 93% rename from app/src/main/java/com/mintris/game/GameLifecycleManager.kt rename to app/src/main/java/com/pixelmintdrop/game/GameLifecycleManager.kt index 76bc96b..f998875 100644 --- a/app/src/main/java/com/mintris/game/GameLifecycleManager.kt +++ b/app/src/main/java/com/pixelmintdrop/game/GameLifecycleManager.kt @@ -1,13 +1,13 @@ -package com.mintris.game +package com.pixelmintdrop.game import android.content.Context import android.content.SharedPreferences import android.os.Bundle import com.google.gson.Gson -import com.mintris.audio.GameMusic -import com.mintris.model.GameBoard -import com.mintris.model.HighScoreManager -import com.mintris.model.StatsManager +import com.pixelmintdrop.audio.GameMusic +import com.pixelmintdrop.model.GameBoard +import com.pixelmintdrop.model.HighScoreManager +import com.pixelmintdrop.model.StatsManager /** * Handles the game's lifecycle events to ensure proper resource management @@ -15,7 +15,7 @@ import com.mintris.model.StatsManager */ class GameLifecycleManager(private val context: Context) { private val sharedPreferences: SharedPreferences = - context.getSharedPreferences("com.mintris.game_state", Context.MODE_PRIVATE) + context.getSharedPreferences("com.com.pixelmintgames.pixelmintdrop.game_state", Context.MODE_PRIVATE) private val gson = Gson() /** diff --git a/app/src/main/java/com/mintris/game/GameView.kt b/app/src/main/java/com/pixelmintdrop/game/GameView.kt similarity index 58% rename from app/src/main/java/com/mintris/game/GameView.kt rename to app/src/main/java/com/pixelmintdrop/game/GameView.kt index e85f665..2a062ee 100644 --- a/app/src/main/java/com/mintris/game/GameView.kt +++ b/app/src/main/java/com/pixelmintdrop/game/GameView.kt @@ -1,4 +1,4 @@ -package com.mintris.game +package com.pixelmintdrop.game import android.animation.ValueAnimator import android.content.Context @@ -20,14 +20,19 @@ import android.view.View import android.view.animation.LinearInterpolator import android.hardware.display.DisplayManager import android.view.Display -import com.mintris.model.GameBoard -import com.mintris.model.Tetromino -import com.mintris.model.TetrominoType +import com.pixelmintdrop.model.GameBoard +import com.pixelmintdrop.model.GamePiece +import com.pixelmintdrop.model.GamePieceType +import com.pixelmintdrop.model.PlayerProgressionManager +import com.pixelmintdrop.model.StatsManager +import com.pixelmintdrop.model.ThemeManager +import com.pixelmintdrop.model.ThemeManager.Theme +import com.pixelmintdrop.model.ThemeManager.ThemeType +import com.pixelmintdrop.model.ThemeManager.ThemeVariant import kotlin.math.abs -import kotlin.math.min /** - * GameView that renders the Tetris game and handles touch input + * GameView that renders the block-stacking game and handles touch input */ class GameView @JvmOverloads constructor( context: Context, @@ -45,9 +50,13 @@ class GameView @JvmOverloads constructor( // Game state private var isRunning = false - var isPaused = false // Changed from private to public to allow access from MainActivity + var isPaused = false private var score = 0 + // Current piece + private var currentPiece: GamePiece? = null + private var nextPiece: GamePiece? = null + // Callbacks var onNextPieceChanged: (() -> Unit)? = null @@ -68,22 +77,22 @@ class GameView @JvmOverloads constructor( private val ghostBlockPaint = Paint().apply { color = Color.WHITE - alpha = 80 // 30% opacity + alpha = 80 isAntiAlias = true } private val gridPaint = Paint().apply { - color = Color.parseColor("#222222") // Very dark gray - alpha = 20 // Reduced from 40 to be more subtle + color = Color.parseColor("#222222") + alpha = 20 isAntiAlias = true strokeWidth = 1f style = Paint.Style.STROKE - maskFilter = null // Ensure no blur effect on grid lines + maskFilter = null } private val glowPaint = Paint().apply { color = Color.WHITE - alpha = 40 // Reduced from 80 for more subtlety + alpha = 40 isAntiAlias = true style = Paint.Style.STROKE strokeWidth = 1.5f @@ -98,24 +107,20 @@ class GameView @JvmOverloads constructor( maskFilter = BlurMaskFilter(12f, BlurMaskFilter.Blur.OUTER) } - // Add a new paint for the pulse effect private val pulsePaint = Paint().apply { color = Color.CYAN alpha = 255 isAntiAlias = true style = Paint.Style.FILL - maskFilter = BlurMaskFilter(32f, BlurMaskFilter.Blur.OUTER) // Increased from 16f to 32f + maskFilter = BlurMaskFilter(32f, BlurMaskFilter.Blur.OUTER) } - // Pre-allocate paint objects to avoid GC private val tmpPaint = Paint() - // Calculate block size based on view dimensions and board size private var blockSize = 0f private var boardLeft = 0f private var boardTop = 0f - // Game loop handler and runnable private val handler = Handler(Looper.getMainLooper()) private val gameLoopRunnable = object : Runnable { override fun run() { @@ -127,7 +132,6 @@ class GameView @JvmOverloads constructor( } } - // Touch parameters private var lastTouchX = 0f private var lastTouchY = 0f private var startX = 0f @@ -216,7 +220,7 @@ class GameView @JvmOverloads constructor( pause() // Load saved block skin - val prefs = context.getSharedPreferences("mintris_progression", Context.MODE_PRIVATE) + val prefs = context.getSharedPreferences("pixelmintdrop_progression", Context.MODE_PRIVATE) currentBlockSkin = prefs.getString("selected_block_skin", "block_skin_1") ?: "block_skin_1" // Connect our callbacks to the GameBoard @@ -329,7 +333,7 @@ class GameView @JvmOverloads constructor( } // Save the selection to SharedPreferences - val prefs = context.getSharedPreferences("mintris_progression", Context.MODE_PRIVATE) + val prefs = context.getSharedPreferences("pixelmintdrop_progression", Context.MODE_PRIVATE) prefs.edit().putString("selected_block_skin", skinId).commit() // Force a refresh of the view @@ -428,197 +432,29 @@ class GameView @JvmOverloads constructor( override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) - // Force hardware acceleration - Critical for performance - setLayerType(LAYER_TYPE_HARDWARE, null) + // Calculate block size based on view dimensions and board size + blockSize = minOf( + w / (gameBoard.width + 2f), // Add padding + h / (gameBoard.height + 2f) // Add padding + ) - // Update gesture exclusion rect for edge-to-edge rendering - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - setSystemGestureExclusionRects(listOf(Rect(0, 0, w, h))) - } - - calculateDimensions(w, h) - } - - /** - * Calculate dimensions for the board and blocks based on view size - */ - private fun calculateDimensions(width: Int, height: Int) { - // Calculate block size based on available space - val horizontalBlocks = gameBoard.width - val verticalBlocks = gameBoard.height - - // Account for all glow effects and borders - val borderPadding = 16f // Padding for border glow effects - - // Calculate block size to fit the height exactly, accounting for all padding - blockSize = (height.toFloat() - (borderPadding * 2)) / verticalBlocks - - // Calculate total board width - val totalBoardWidth = blockSize * horizontalBlocks - - // Center horizontally - boardLeft = (width - totalBoardWidth) / 2 - boardTop = borderPadding // Start with border padding from top - - // Calculate the total height needed for the board - val totalHeight = blockSize * verticalBlocks - - // Log dimensions for debugging - Log.d(TAG, "Board dimensions: width=$width, height=$height, blockSize=$blockSize, boardLeft=$boardLeft, boardTop=$boardTop, totalHeight=$totalHeight") + // Center the board + boardLeft = (w - gameBoard.width * blockSize) / 2f + boardTop = (h - gameBoard.height * blockSize) / 2f } override fun onDraw(canvas: Canvas) { - // Set hardware layer type during draw for better performance - val wasHardwareAccelerated = isHardwareAccelerated - if (!wasHardwareAccelerated) { - setLayerType(LAYER_TYPE_HARDWARE, null) - } - super.onDraw(canvas) - // Draw background (already black from theme) + // Draw the game board + drawBoard(canvas) - // Draw board border glow - drawBoardBorder(canvas) + // Draw ghost piece + drawGhostPiece(canvas) - // Draw grid (very subtle) - drawGrid(canvas) - - // Draw locked pieces - drawLockedBlocks(canvas) - - if (!gameBoard.isGameOver && isRunning) { - // Draw ghost piece (landing preview) - drawGhostPiece(canvas) - - // Draw active piece - drawActivePiece(canvas) - } - - // Draw game over effect if animating - if (isGameOverAnimating) { - // First layer - full screen glow - val gameOverPaint = Paint().apply { - color = Color.RED // Change to red for more striking game over indication - alpha = (230 * gameOverAlpha).toInt() // Increased opacity - isAntiAlias = true - style = Paint.Style.FILL - // Only apply blur if alpha is greater than 0 - if (gameOverAlpha > 0) { - maskFilter = BlurMaskFilter(64f * gameOverAlpha, BlurMaskFilter.Blur.OUTER) - } - } - - // Apply screen shake if active - if (gameOverShakeAmount > 0) { - canvas.save() - val shakeOffsetX = (Math.random() * 2 - 1) * gameOverShakeAmount * 20 // Doubled for more visible shake - val shakeOffsetY = (Math.random() * 2 - 1) * gameOverShakeAmount * 20 - canvas.translate(shakeOffsetX.toFloat(), shakeOffsetY.toFloat()) - } - - canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), gameOverPaint) - - // Second layer - color transition effect - val gameOverPaint2 = Paint().apply { - // Transition from bright red to theme color - val transitionColor = if (gameOverColorTransition < 0.5f) { - Color.RED - } else { - val transition = (gameOverColorTransition - 0.5f) * 2f - val red = Color.red(currentThemeColor) - val green = Color.green(currentThemeColor) - val blue = Color.blue(currentThemeColor) - Color.argb( - (150 * gameOverAlpha).toInt(), // Increased opacity - (255 - (255-red) * transition).toInt(), - (green * transition).toInt(), // Transition from 0 (red) to theme green - (blue * transition).toInt() // Transition from 0 (red) to theme blue - ) - } - color = transitionColor - alpha = (150 * gameOverAlpha).toInt() // Increased opacity - isAntiAlias = true - style = Paint.Style.FILL - // Only apply blur if alpha is greater than 0 - if (gameOverAlpha > 0) { - maskFilter = BlurMaskFilter(48f * gameOverAlpha, BlurMaskFilter.Blur.OUTER) // Increased blur - } - } - canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), gameOverPaint2) - - // Draw "GAME OVER" text - if (gameOverAlpha > 0.5f) { - val textPaint = Paint().apply { - color = Color.WHITE - alpha = (255 * Math.min(1f, (gameOverAlpha - 0.5f) * 2)).toInt() - isAntiAlias = true - textSize = blockSize * 1.5f // Reduced from 2f to 1.5f to fit on screen - textAlign = Paint.Align.CENTER - typeface = android.graphics.Typeface.DEFAULT_BOLD - style = Paint.Style.FILL_AND_STROKE - strokeWidth = blockSize * 0.08f // Proportionally reduced from 0.1f - } - - // Draw text with glow - val glowPaint = Paint(textPaint).apply { - maskFilter = BlurMaskFilter(blockSize * 0.4f, BlurMaskFilter.Blur.NORMAL) // Reduced from 0.5f - alpha = (200 * Math.min(1f, (gameOverAlpha - 0.5f) * 2)).toInt() - color = Color.RED - strokeWidth = blockSize * 0.15f // Reduced from 0.2f - } - - val xPos = width / 2f - val yPos = height / 3f - - // Measure text width to check if it fits - val textWidth = textPaint.measureText("GAME OVER") - - // If text would still be too wide, scale it down further - if (textWidth > width * 0.9f) { - val scaleFactor = width * 0.9f / textWidth - textPaint.textSize *= scaleFactor - glowPaint.textSize *= scaleFactor - } - - canvas.drawText("GAME OVER", xPos, yPos, glowPaint) - canvas.drawText("GAME OVER", xPos, yPos, textPaint) - } - - // Draw falling blocks - if (gameOverBlocksY.isNotEmpty()) { - for (i in gameOverBlocksY.indices) { - val x = gameOverBlocksX[i] - val y = gameOverBlocksY[i] - val rotation = gameOverBlocksRotation[i] - val size = gameOverBlocksSize[i] * blockSize - - // Skip blocks that have fallen off the screen - if (y > height) continue - - // Draw each falling block with rotation - canvas.save() - canvas.translate(x, y) - canvas.rotate(rotation) - - // Create a pulsing effect for the falling blocks - val blockPaint = Paint(blockPaint) - blockPaint.alpha = (255 * gameOverAlpha * (1.0f - y / height.toFloat() * 0.7f)).toInt() - - // Draw block with glow effect - val blockGlowPaint = Paint(blockGlowPaint) - blockGlowPaint.alpha = (200 * gameOverAlpha * (1.0f - y / height.toFloat() * 0.5f)).toInt() - canvas.drawRect(-size/2, -size/2, size/2, size/2, blockGlowPaint) - canvas.drawRect(-size/2, -size/2, size/2, size/2, blockPaint) - - canvas.restore() - } - } - - // Reset any transformations from screen shake - if (gameOverShakeAmount > 0) { - canvas.restore() - } + // Draw current piece + currentPiece?.let { piece -> + drawPiece(canvas, piece) } } @@ -776,219 +612,52 @@ class GameView @JvmOverloads constructor( * Draw the ghost piece (landing preview) */ private fun drawGhostPiece(canvas: Canvas) { - val piece = gameBoard.getCurrentPiece() ?: return - val ghostY = gameBoard.getGhostY() - - // Draw semi-transparent background for each block - for (y in 0 until piece.getHeight()) { - for (x in 0 until piece.getWidth()) { - if (piece.isBlockAt(x, y)) { - val boardX = piece.x + x - val boardY = ghostY + y - - if (boardX >= 0 && boardX < gameBoard.width) { - val screenX = boardLeft + boardX * blockSize - val screenY = boardTop + boardY * blockSize - - // Draw background - canvas.drawRect( - screenX + 1f, - screenY + 1f, - screenX + blockSize - 1f, - screenY + blockSize - 1f, - ghostBackgroundPaint - ) - - // Draw border - canvas.drawRect( - screenX + 1f, - screenY + 1f, - screenX + blockSize - 1f, - screenY + blockSize - 1f, - ghostBorderPaint - ) - - // Draw outline - canvas.drawRect( - screenX + 1f, - screenY + 1f, - screenX + blockSize - 1f, - screenY + blockSize - 1f, - ghostPaint - ) - } - } - } + currentPiece?.let { piece -> + val ghostY = calculateGhostY(piece) + val originalY = piece.y + piece.y = ghostY + + ghostBlockPaint.alpha = 50 + drawPiece(canvas, piece, alpha = 50) + + piece.y = originalY } } - /** - * Draw a single tetris block at the given grid position - */ - private fun drawBlock(canvas: Canvas, x: Int, y: Int, isGhost: Boolean, isPulsingLine: Boolean) { - val left = boardLeft + x * blockSize - val top = boardTop + y * blockSize - val right = left + blockSize - val bottom = top + blockSize + private fun calculateGhostY(piece: GamePiece): Int { + var testY = piece.y + while (testY < gameBoard.height && !gameBoard.wouldCollide(piece, piece.x, testY + 1)) { + testY++ + } + return testY + } + + private fun drawPiece(canvas: Canvas, piece: GamePiece, offsetX: Float = 0f, offsetY: Float = 0f, alpha: Int = 255) { + val width = piece.getWidth() + val height = piece.getHeight() - // Save canvas state before drawing block effects - canvas.save() - - // Get the current block skin paint - val paint = blockSkinPaints[currentBlockSkin] ?: blockSkinPaints["block_skin_1"]!! - - // Create a clone of the paint to avoid modifying the original - val blockPaint = Paint(paint) - - // Draw block based on current skin - when (currentBlockSkin) { - "block_skin_1" -> { // Classic - // Draw outer glow - blockGlowPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE - canvas.drawRect(left - 2f, top - 2f, right + 2f, bottom + 2f, blockGlowPaint) - - // Draw block - blockPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE - blockPaint.alpha = if (isGhost) 30 else 255 - canvas.drawRect(left, top, right, bottom, blockPaint) - - // Draw inner glow - glowPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE - canvas.drawRect(left + 1f, top + 1f, right - 1f, bottom - 1f, glowPaint) - } - "block_skin_2" -> { // Neon - // Stronger outer glow for neon skin - blockGlowPaint.color = if (isGhost) Color.argb(30, 255, 0, 255) else Color.parseColor("#FF00FF") - blockGlowPaint.maskFilter = BlurMaskFilter(16f, BlurMaskFilter.Blur.OUTER) - canvas.drawRect(left - 4f, top - 4f, right + 4f, bottom + 4f, blockGlowPaint) - - // For neon, use semi-translucent fill with strong glowing edges - blockPaint.style = Paint.Style.FILL_AND_STROKE - blockPaint.strokeWidth = 2f - blockPaint.maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.NORMAL) - - if (isGhost) { - blockPaint.color = Color.argb(30, 255, 0, 255) - blockPaint.alpha = 30 - } else { - blockPaint.color = Color.parseColor("#66004D") // Darker magenta fill - blockPaint.alpha = 170 // More opaque to be more visible - } - - // Draw block with neon effect - canvas.drawRect(left, top, right, bottom, blockPaint) - - // Draw a brighter border for better visibility - val borderPaint = Paint().apply { - color = Color.parseColor("#FF00FF") - style = Paint.Style.STROKE - strokeWidth = 3f - alpha = 255 - isAntiAlias = true - maskFilter = BlurMaskFilter(6f, BlurMaskFilter.Blur.NORMAL) - } - canvas.drawRect(left, top, right, bottom, borderPaint) - - // Inner glow for neon blocks - glowPaint.color = if (isGhost) Color.argb(10, 255, 0, 255) else Color.parseColor("#FF00FF") - glowPaint.alpha = if (isGhost) 10 else 100 - glowPaint.style = Paint.Style.STROKE - glowPaint.strokeWidth = 2f - glowPaint.maskFilter = BlurMaskFilter(4f, BlurMaskFilter.Blur.NORMAL) - canvas.drawRect(left + 4f, top + 4f, right - 4f, bottom - 4f, glowPaint) - } - "block_skin_3" -> { // Retro - // Draw pixelated block with retro effect - blockPaint.color = if (isGhost) Color.argb(30, 255, 90, 95) else Color.parseColor("#FF5A5F") - blockPaint.alpha = if (isGhost) 30 else 255 - - // Draw main block - canvas.drawRect(left, top, right, bottom, blockPaint) - - // Draw pixelated highlights - val highlightPaint = Paint().apply { - color = Color.parseColor("#FF8A8F") - isAntiAlias = false - style = Paint.Style.FILL - } - // Top and left highlights - canvas.drawRect(left, top, right - 2f, top + 2f, highlightPaint) - canvas.drawRect(left, top, left + 2f, bottom - 2f, highlightPaint) - - // Draw pixelated shadows - val shadowPaint = Paint().apply { - color = Color.parseColor("#CC4A4F") - isAntiAlias = false - style = Paint.Style.FILL - } - // Bottom and right shadows - canvas.drawRect(left + 2f, bottom - 2f, right, bottom, shadowPaint) - canvas.drawRect(right - 2f, top + 2f, right, bottom - 2f, shadowPaint) - } - "block_skin_4" -> { // Minimalist - // Draw clean, simple block with subtle border - blockPaint.color = if (isGhost) Color.argb(30, 0, 0, 0) else Color.BLACK - blockPaint.alpha = if (isGhost) 30 else 255 - blockPaint.style = Paint.Style.FILL - canvas.drawRect(left, top, right, bottom, blockPaint) - - // Draw subtle border - val borderPaint = Paint().apply { - color = Color.parseColor("#333333") - style = Paint.Style.STROKE - strokeWidth = 1f - isAntiAlias = true - } - canvas.drawRect(left, top, right, bottom, borderPaint) - } - "block_skin_5" -> { // Galaxy - // Draw cosmic glow effect - blockGlowPaint.color = if (isGhost) Color.argb(30, 102, 252, 241) else Color.parseColor("#66FCF1") - blockGlowPaint.maskFilter = BlurMaskFilter(20f, BlurMaskFilter.Blur.OUTER) - canvas.drawRect(left - 8f, top - 8f, right + 8f, bottom + 8f, blockGlowPaint) - - // Draw main block with gradient - val gradient = LinearGradient( - left, top, right, bottom, - Color.parseColor("#66FCF1"), - Color.parseColor("#45B7AF"), - Shader.TileMode.CLAMP - ) - blockPaint.shader = gradient - blockPaint.color = if (isGhost) Color.argb(30, 102, 252, 241) else Color.parseColor("#66FCF1") - blockPaint.alpha = if (isGhost) 30 else 255 - blockPaint.style = Paint.Style.FILL - canvas.drawRect(left, top, right, bottom, blockPaint) - - // Draw star-like sparkles - if (!isGhost) { - val sparklePaint = Paint().apply { - color = Color.WHITE - style = Paint.Style.FILL - isAntiAlias = true - maskFilter = BlurMaskFilter(4f, BlurMaskFilter.Blur.NORMAL) - } - // Add small white dots for sparkle effect - canvas.drawCircle(left + 4f, top + 4f, 1f, sparklePaint) - canvas.drawCircle(right - 4f, bottom - 4f, 1f, sparklePaint) + blockPaint.alpha = alpha + for (y in 0 until height) { + for (x in 0 until width) { + if (piece.isBlockAt(x, y)) { + val left = offsetX + (piece.x + x) * blockSize + val top = offsetY + (piece.y + y) * blockSize + val right = left + blockSize + val bottom = top + blockSize + + // Draw block with subtle glow + blockGlowPaint.alpha = alpha / 4 + canvas.drawRect(left, top, right, bottom, blockGlowPaint) + + // Draw the main block + canvas.drawRect(left + 1f, top + 1f, right - 1f, bottom - 1f, blockPaint) + + // Draw border glow + glowPaint.alpha = alpha / 2 + canvas.drawRect(left, top, right, bottom, glowPaint) } } } - - // Draw pulse effect if animation is active and this is a pulsing line - if (isPulsing && isPulsingLine) { - val pulseBlockPaint = Paint().apply { - color = Color.WHITE - alpha = (255 * pulseAlpha).toInt() - isAntiAlias = true - style = Paint.Style.FILL - maskFilter = BlurMaskFilter(40f * (1f + pulseAlpha), BlurMaskFilter.Blur.OUTER) - } - canvas.drawRect(left - 16f, top - 16f, right + 16f, bottom + 16f, pulseBlockPaint) - } - - // Restore canvas state after drawing block effects - canvas.restore() } /** @@ -1026,16 +695,7 @@ class GameView @JvmOverloads constructor( // Custom touch event handling override fun onTouchEvent(event: MotionEvent): Boolean { - if (!isRunning || isPaused || gameBoard.isGameOver) { - return true - } - - // Ignore touch events during the freeze period after a piece locks - val currentTime = System.currentTimeMillis() - if (currentTime < touchFreezeUntil) { - Log.d(TAG, "Ignoring touch event - freeze active for ${touchFreezeUntil - currentTime}ms more") - return true - } + if (!isRunning || isPaused) return false when (event.action) { MotionEvent.ACTION_DOWN -> { @@ -1043,120 +703,53 @@ class GameView @JvmOverloads constructor( startY = event.y lastTouchX = event.x lastTouchY = event.y - lastTapTime = currentTime // Set the tap time when touch starts - - // Reset direction lock - lockedDirection = null + lastTapTime = System.currentTimeMillis() + return true } MotionEvent.ACTION_MOVE -> { - val deltaX = event.x - lastTouchX - val deltaY = event.y - lastTouchY + val dx = event.x - lastTouchX + val dy = event.y - lastTouchY - // Check if we should lock direction - if (lockedDirection == null) { - val absDeltaX = abs(deltaX) - val absDeltaY = abs(deltaY) - - if (absDeltaX > blockSize * directionLockThreshold || - absDeltaY > blockSize * directionLockThreshold) { - // Lock to the dominant direction - lockedDirection = if (absDeltaX > absDeltaY) { - Direction.HORIZONTAL - } else { - Direction.VERTICAL - } + // Horizontal movement + if (abs(dx) > blockSize / 2) { + if (dx > 0) { + movePieceRight() + } else { + movePieceLeft() } + lastTouchX = event.x } - // Handle movement based on locked direction - when (lockedDirection) { - Direction.HORIZONTAL -> { - if (abs(deltaX) > blockSize * minMovementThreshold) { - if (deltaX > 0) { - gameBoard.moveRight() - } else { - gameBoard.moveLeft() - } - lastTouchX = event.x - if (currentTime - lastMoveTime >= moveCooldown) { - gameHaptics?.vibrateForPieceMove() - lastMoveTime = currentTime - } - invalidate() - } - } - Direction.VERTICAL -> { - if (deltaY > blockSize * minMovementThreshold) { - gameBoard.softDrop() - lastTouchY = event.y - if (currentTime - lastMoveTime >= moveCooldown) { - gameHaptics?.vibrateForPieceMove() - lastMoveTime = currentTime - } - invalidate() - } - } - null -> { - // No direction lock yet, don't process movement - } + // Vertical movement (soft drop) + if (dy > blockSize / 2) { + dropPiece() + lastTouchY = event.y } + + return true } MotionEvent.ACTION_UP -> { - val deltaX = event.x - startX - val deltaY = event.y - startY - val moveTime = currentTime - lastTapTime + val dx = event.x - startX + val dy = event.y - startY + val tapTime = System.currentTimeMillis() - lastTapTime - // Handle taps for rotation - if (moveTime < minTapTime * 1.5 && - abs(deltaY) < maxTapMovement * 1.5 && - abs(deltaX) < maxTapMovement * 1.5) { - - if (currentTime - lastRotationTime >= rotationCooldown) { - gameBoard.rotate() - lastRotationTime = currentTime - gameHaptics?.vibrateForPieceMove() - invalidate() - } - return true + // Detect tap for rotation + if (abs(dx) < blockSize / 2 && abs(dy) < blockSize / 2 && tapTime < 200) { + rotatePiece() } - // Handle gestures - // Check for hold gesture (swipe up) - if (deltaY < -blockSize * minHoldDistance && - abs(deltaX) / abs(deltaY) < 0.5f) { - if (currentTime - lastHoldTime >= holdCooldown) { - gameBoard.holdPiece() - lastHoldTime = currentTime - gameHaptics?.vibrateForPieceMove() - invalidate() - } - } - // Check for hard drop (must be faster and longer than soft drop) - else if (deltaY > blockSize * minHardDropDistance && - abs(deltaX) / abs(deltaY) < 0.5f && - (deltaY / moveTime) * 1000 > minSwipeVelocity) { - if (currentTime - lastHardDropTime >= hardDropCooldown) { - gameBoard.hardDrop() - lastHardDropTime = currentTime - invalidate() - } - } - // Check for soft drop (slower and shorter than hard drop) - else if (deltaY > blockSize * minMovementThreshold && - deltaY < blockSize * maxSoftDropDistance && - (deltaY / moveTime) * 1000 < minSwipeVelocity) { - gameBoard.softDrop() - invalidate() + // Detect swipe down for hard drop + if (dy > height / 4) { + hardDrop() } - // Reset direction lock - lockedDirection = null + return true } } - return true + return false } /** @@ -1182,7 +775,7 @@ class GameView @JvmOverloads constructor( /** * Get the next piece that will be spawned */ - fun getNextPiece(): Tetromino? { + fun getNextPiece(): GamePiece? { return gameBoard.getNextPiece() } @@ -1542,4 +1135,103 @@ class GameView @JvmOverloads constructor( gameHaptics?.vibrateForPieceMove() invalidate() } + + fun getNextPiece(): GamePiece? = nextPiece + + fun getCurrentPiece(): GamePiece? = currentPiece + + fun spawnNewPiece() { + currentPiece = gameBoard.spawnNewPiece() + nextPiece = gameBoard.getNextPiece() + onNextPieceChanged?.invoke() + } + + fun rotatePiece() { + currentPiece?.let { piece -> + if (gameBoard.rotatePiece(piece)) { + invalidate() + } + } + } + + fun movePieceLeft() { + currentPiece?.let { piece -> + if (gameBoard.movePiece(piece, -1, 0)) { + invalidate() + } + } + } + + fun movePieceRight() { + currentPiece?.let { piece -> + if (gameBoard.movePiece(piece, 1, 0)) { + invalidate() + } + } + } + + fun dropPiece() { + currentPiece?.let { piece -> + if (gameBoard.movePiece(piece, 0, 1)) { + invalidate() + } + } + } + + fun hardDrop() { + currentPiece?.let { piece -> + val ghostY = calculateGhostY(piece) + if (ghostY > piece.y) { + piece.y = ghostY + gameBoard.lockPiece(piece) + spawnNewPiece() + invalidate() + } + } + } + + fun startGame() { + if (!isRunning) { + isRunning = true + isPaused = false + gameBoard.reset() + spawnNewPiece() + handler.post(gameLoopRunnable) + } + } + + fun pauseGame() { + if (isRunning && !isPaused) { + isPaused = true + handler.removeCallbacks(gameLoopRunnable) + } + } + + fun resumeGame() { + if (isRunning && isPaused) { + isPaused = false + handler.post(gameLoopRunnable) + } + } + + fun stopGame() { + isRunning = false + isPaused = false + handler.removeCallbacks(gameLoopRunnable) + } + + fun resetGame() { + stopGame() + gameBoard.reset() + currentPiece = null + nextPiece = null + score = 0 + invalidate() + } + + fun getScore(): Int = score + + fun setGameHaptics(haptics: GameHaptics?) { + gameHaptics = haptics + } } diff --git a/app/src/main/java/com/mintris/game/GamepadController.kt b/app/src/main/java/com/pixelmintdrop/game/GamepadController.kt similarity index 98% rename from app/src/main/java/com/mintris/game/GamepadController.kt rename to app/src/main/java/com/pixelmintdrop/game/GamepadController.kt index 906758d..83718b2 100644 --- a/app/src/main/java/com/mintris/game/GamepadController.kt +++ b/app/src/main/java/com/pixelmintdrop/game/GamepadController.kt @@ -1,4 +1,4 @@ -package com.mintris.game +package com.pixelmintdrop.game import android.os.SystemClock import android.view.InputDevice @@ -8,13 +8,11 @@ import android.util.Log import android.content.Context import android.os.Build import android.os.VibrationEffect -import android.view.InputDevice.MotionRange -import android.os.Vibrator import android.os.Handler import android.os.Looper /** - * GamepadController handles gamepad input for the Mintris game. + * GamepadController handles gamepad input for the pixelmintdrop game. * Supports multiple gamepad types including: * - Microsoft Xbox controllers * - Sony PlayStation controllers @@ -258,7 +256,7 @@ class GamepadController( 1 -> 100 2 -> 150 3 -> 200 - else -> 255 // For tetris (4 lines) + else -> 255 // For quad (4 lines) } vibrateGamepad(RUMBLE_LINE_CLEAR_DURATION_MS, amplitude) } diff --git a/app/src/main/java/com/mintris/game/HoldPieceView.kt b/app/src/main/java/com/pixelmintdrop/game/HoldPieceView.kt similarity index 97% rename from app/src/main/java/com/mintris/game/HoldPieceView.kt rename to app/src/main/java/com/pixelmintdrop/game/HoldPieceView.kt index 6f51022..ae7272c 100644 --- a/app/src/main/java/com/mintris/game/HoldPieceView.kt +++ b/app/src/main/java/com/pixelmintdrop/game/HoldPieceView.kt @@ -1,4 +1,4 @@ -package com.mintris.game +package com.pixelmintdrop.game import android.content.Context import android.graphics.BlurMaskFilter @@ -7,8 +7,7 @@ import android.graphics.Color import android.graphics.Paint import android.util.AttributeSet import android.view.View -import com.mintris.model.GameBoard -import com.mintris.model.Tetromino +import com.pixelmintdrop.model.GameBoard import kotlin.math.min /** diff --git a/app/src/main/java/com/pixelmintdrop/game/NextPieceView.kt b/app/src/main/java/com/pixelmintdrop/game/NextPieceView.kt new file mode 100644 index 0000000..f6d3543 --- /dev/null +++ b/app/src/main/java/com/pixelmintdrop/game/NextPieceView.kt @@ -0,0 +1,97 @@ +package com.pixelmintdrop.game + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RectF +import android.graphics.BlurMaskFilter +import android.util.AttributeSet +import android.view.View +import kotlin.math.min + +/** + * Custom view to display the next game piece + */ +class NextPieceView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + private var gameView: GameView? = null + + // Rendering + private val blockPaint = Paint().apply { + color = Color.WHITE + isAntiAlias = true + } + + private val glowPaint = Paint().apply { + color = Color.WHITE + alpha = 30 + isAntiAlias = true + style = Paint.Style.STROKE + strokeWidth = 1.5f + } + + /** + * Set the game view to get the next piece from + */ + fun setGameView(gameView: GameView) { + this.gameView = gameView + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + // Get the next piece from game view + gameView?.getNextPiece()?.let { piece -> + val width = piece.getWidth() + val height = piece.getHeight() + + // Calculate block size for the preview (smaller than main board) + val previewBlockSize = min( + canvas.width.toFloat() / (width.toFloat() + 2f), + canvas.height.toFloat() / (height.toFloat() + 2f) + ) + + // Center the piece in the preview area + val previewLeft = (canvas.width.toFloat() - width.toFloat() * previewBlockSize) / 2f + val previewTop = (canvas.height.toFloat() - height.toFloat() * previewBlockSize) / 2f + + // Draw subtle background glow + val glowPaint = Paint().apply { + color = Color.WHITE + alpha = 10 + maskFilter = BlurMaskFilter(previewBlockSize * 0.5f, BlurMaskFilter.Blur.OUTER) + } + canvas.drawRect( + previewLeft - previewBlockSize, + previewTop - previewBlockSize, + previewLeft + width.toFloat() * previewBlockSize + previewBlockSize, + previewTop + height.toFloat() * previewBlockSize + previewBlockSize, + glowPaint + ) + + for (y in 0 until height) { + for (x in 0 until width) { + if (piece.isBlockAt(x, y)) { + val left = previewLeft + x.toFloat() * previewBlockSize + val top = previewTop + y.toFloat() * previewBlockSize + val right = left + previewBlockSize + val bottom = top + previewBlockSize + + // Draw block with subtle glow + val rect = RectF(left + 1f, top + 1f, right - 1f, bottom - 1f) + canvas.drawRect(rect, blockPaint) + + // Draw subtle border glow + val glowRect = RectF(left, top, right, bottom) + canvas.drawRect(glowRect, glowPaint) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mintris/game/TitleScreen.kt b/app/src/main/java/com/pixelmintdrop/game/TitleScreen.kt similarity index 80% rename from app/src/main/java/com/mintris/game/TitleScreen.kt rename to app/src/main/java/com/pixelmintdrop/game/TitleScreen.kt index 324f81b..f9baa6d 100644 --- a/app/src/main/java/com/mintris/game/TitleScreen.kt +++ b/app/src/main/java/com/pixelmintdrop/game/TitleScreen.kt @@ -1,4 +1,4 @@ -package com.mintris.game +package com.pixelmintdrop.game import android.content.Context import android.graphics.Canvas @@ -10,9 +10,9 @@ import android.view.MotionEvent import android.view.View import java.util.Random import android.util.Log -import com.mintris.model.HighScoreManager -import com.mintris.model.HighScore -import com.mintris.model.PlayerProgressionManager +import com.pixelmintdrop.model.HighScoreManager +import com.pixelmintdrop.model.HighScore +import com.pixelmintdrop.model.PlayerProgressionManager import kotlin.math.abs import androidx.core.graphics.withTranslation import androidx.core.graphics.withScale @@ -33,7 +33,7 @@ class TitleScreen @JvmOverloads constructor( private val random = Random() private var width = 0 private var height = 0 - private val tetrominosToAdd = mutableListOf() + private val piecesToAdd = mutableListOf() private val highScoreManager = HighScoreManager(context) // Pre-allocate HighScoreManager // Touch handling variables @@ -50,8 +50,8 @@ class TitleScreen @JvmOverloads constructor( private var themeColor = Color.WHITE private var backgroundColor = Color.BLACK - // Define tetromino shapes (I, O, T, S, Z, J, L) - private val tetrominoShapes = arrayOf( + // Define piece shapes (I, O, T, S, Z, J, L) + private val pieceShapes = arrayOf( // I arrayOf( intArrayOf(0, 0, 0, 0), @@ -96,8 +96,8 @@ class TitleScreen @JvmOverloads constructor( ) ) - // Tetromino class to represent falling pieces - private class Tetromino( + // FallingPiece class to represent falling pieces + private class FallingPiece( var x: Float, var y: Float, val shape: Array, @@ -106,7 +106,7 @@ class TitleScreen @JvmOverloads constructor( val rotation: Int = 0 ) - private val tetrominos = mutableListOf() + private val pieces = mutableListOf() init { // Title text settings @@ -138,14 +138,14 @@ class TitleScreen @JvmOverloads constructor( alpha = 200 } - // General paint settings for tetrominos + // General paint settings for pieces paint.apply { color = themeColor style = Paint.Style.FILL isAntiAlias = true } - // Glow paint settings for tetrominos + // Glow paint settings for pieces glowPaint.apply { color = themeColor style = Paint.Style.FILL @@ -159,26 +159,26 @@ class TitleScreen @JvmOverloads constructor( width = w height = h - // Clear existing tetrominos - tetrominos.clear() + // Clear existing pieces + pieces.clear() - // Initialize some tetrominos + // Initialize some pieces repeat(20) { - val tetromino = createRandomTetromino() - tetrominos.add(tetromino) + val piece = createRandomPiece() + pieces.add(piece) } } - private fun createRandomTetromino(): Tetromino { + private fun createRandomPiece(): FallingPiece { val x = random.nextFloat() * (width - 150) + 50 // Keep away from edges val y = -cellSize * 4 - (random.nextFloat() * height / 2) - val shapeIndex = random.nextInt(tetrominoShapes.size) - val shape = tetrominoShapes[shapeIndex] + val shapeIndex = random.nextInt(pieceShapes.size) + val shape = pieceShapes[shapeIndex] val speed = 1f + random.nextFloat() * 2f val scale = 0.8f + random.nextFloat() * 0.4f val rotation = random.nextInt(4) * 90 - return Tetromino(x, y, shape, speed, scale, rotation) + return FallingPiece(x, y, shape, speed, scale, rotation) } override fun onDraw(canvas: Canvas) { @@ -188,37 +188,37 @@ class TitleScreen @JvmOverloads constructor( // Draw background using the current background color canvas.drawColor(backgroundColor) - // Add any pending tetrominos - tetrominos.addAll(tetrominosToAdd) - tetrominosToAdd.clear() + // Add any pending pieces + pieces.addAll(piecesToAdd) + piecesToAdd.clear() - // Update and draw falling tetrominos - val tetrominosToRemove = mutableListOf() + // Update and draw falling pieces + val piecesToRemove = mutableListOf() - for (tetromino in tetrominos) { - tetromino.y += tetromino.speed + for (piece in pieces) { + piece.y += piece.speed - // Remove tetrominos that have fallen off the screen - if (tetromino.y > height) { - tetrominosToRemove.add(tetromino) - tetrominosToAdd.add(createRandomTetromino()) + // Remove pieces that have fallen off the screen + if (piece.y > height) { + piecesToRemove.add(piece) + piecesToAdd.add(createRandomPiece()) } else { try { - // Draw the tetromino - for (y in 0 until tetromino.shape.size) { - for (x in 0 until tetromino.shape.size) { - if (tetromino.shape[y][x] == 1) { + // Draw the piece + for (y in 0 until piece.shape.size) { + for (x in 0 until piece.shape.size) { + if (piece.shape[y][x] == 1) { val left = x * cellSize val top = y * cellSize val right = left + cellSize val bottom = top + cellSize // 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) { + canvas.withTranslation(piece.x, piece.y) { + withScale(piece.scale, piece.scale) { + withRotation(piece.rotation.toFloat(), + piece.shape.size * cellSize / 2, + piece.shape.size * cellSize / 2) { // Draw glow canvas.drawRect(left - 8f, top - 8f, right + 8f, bottom + 8f, glowPaint) // Draw block @@ -230,17 +230,17 @@ class TitleScreen @JvmOverloads constructor( } } } catch (e: Exception) { - Log.e("TitleScreen", "Error drawing tetromino", e) + Log.e("TitleScreen", "Error drawing piece", e) } } } - // Remove tetrominos that fell off the screen - tetrominos.removeAll(tetrominosToRemove) + // Remove pieces that fell off the screen + pieces.removeAll(piecesToRemove) // Draw title val titleY = height * 0.4f - canvas.drawText("mintris", width / 2f, titleY, titlePaint) + canvas.drawText("Pixel Mint Drop", width / 2f, titleY, titlePaint) // Draw high scores using pre-allocated manager val highScores: List = highScoreManager.getHighScores() @@ -292,10 +292,10 @@ class TitleScreen @JvmOverloads constructor( 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 + // Update piece positions + for (piece in pieces) { + piece.x += deltaX * 0.5f + piece.y += deltaY * 0.5f } lastTouchX = event.x diff --git a/app/src/main/java/com/pixelmintdrop/model/GameBoard.kt b/app/src/main/java/com/pixelmintdrop/model/GameBoard.kt new file mode 100644 index 0000000..35d7d06 --- /dev/null +++ b/app/src/main/java/com/pixelmintdrop/model/GameBoard.kt @@ -0,0 +1,199 @@ +package com.pixelmintdrop.model + +import android.util.Log + +/** + * Represents the game board and manages piece placement and collision detection + */ +class GameBoard { + companion object { + const val WIDTH = 10 + const val HEIGHT = 20 + const val INITIAL_DROP_INTERVAL = 1000L // 1 second + const val MIN_DROP_INTERVAL = 100L // 0.1 seconds + } + + // Board state + private val board = Array(HEIGHT) { Array(WIDTH) { false } } + private var currentPiece: GamePiece? = null + private var nextPiece: GamePiece? = null + + // Game state + var dropInterval = INITIAL_DROP_INTERVAL + private set + + val width: Int = WIDTH + val height: Int = HEIGHT + + fun reset() { + // Clear the board + for (y in 0 until HEIGHT) { + for (x in 0 until WIDTH) { + board[y][x] = false + } + } + + // Reset pieces + currentPiece = null + nextPiece = null + + // Reset drop interval + dropInterval = INITIAL_DROP_INTERVAL + } + + fun spawnNewPiece(): GamePiece { + // Get the next piece or create a new one if none exists + currentPiece = nextPiece ?: createRandomPiece() + nextPiece = createRandomPiece() + + // Position the piece at the top center of the board + currentPiece?.let { piece -> + piece.x = (WIDTH - piece.getWidth()) / 2 + piece.y = 0 + } + + return currentPiece!! + } + + fun getNextPiece(): GamePiece? = nextPiece + + fun getCurrentPiece(): GamePiece? = currentPiece + + fun hasBlockAt(x: Int, y: Int): Boolean { + if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) return false + return board[y][x] + } + + fun wouldCollide(piece: GamePiece, newX: Int, newY: Int): Boolean { + val width = piece.getWidth() + val height = piece.getHeight() + + for (y in 0 until height) { + for (x in 0 until width) { + if (piece.isBlockAt(x, y)) { + val boardX = newX + x + val boardY = newY + y + + // Check board boundaries + if (boardX < 0 || boardX >= WIDTH || boardY >= HEIGHT) { + return true + } + + // Check collision with placed blocks + if (boardY >= 0 && board[boardY][boardX]) { + return true + } + } + } + } + + return false + } + + fun movePiece(piece: GamePiece, dx: Int, dy: Int): Boolean { + val newX = piece.x + dx + val newY = piece.y + dy + + if (!wouldCollide(piece, newX, newY)) { + piece.x = newX + piece.y = newY + return true + } + + return false + } + + fun rotatePiece(piece: GamePiece): Boolean { + // Save current state + val originalRotation = piece.rotation + + // Try to rotate + piece.rotate() + + // Check if new position is valid + if (wouldCollide(piece, piece.x, piece.y)) { + // If not valid, try wall kicks + val kicks = arrayOf( + Pair(1, 0), // Try moving right + Pair(-1, 0), // Try moving left + Pair(0, -1), // Try moving up + Pair(2, 0), // Try moving right 2 + Pair(-2, 0) // Try moving left 2 + ) + + for ((kickX, kickY) in kicks) { + if (!wouldCollide(piece, piece.x + kickX, piece.y + kickY)) { + piece.x += kickX + piece.y += kickY + return true + } + } + + // If no wall kick worked, revert rotation + piece.rotation = originalRotation + return false + } + + return true + } + + fun lockPiece(piece: GamePiece) { + val width = piece.getWidth() + val height = piece.getHeight() + + // Place the piece on the board + for (y in 0 until height) { + for (x in 0 until width) { + if (piece.isBlockAt(x, y)) { + val boardX = piece.x + x + val boardY = piece.y + y + if (boardY >= 0 && boardY < HEIGHT && boardX >= 0 && boardX < WIDTH) { + board[boardY][boardX] = true + } + } + } + } + + // Clear any completed lines + clearLines() + } + + private fun clearLines() { + var linesCleared = 0 + var y = HEIGHT - 1 + + while (y >= 0) { + if (isLineFull(y)) { + // Move all lines above down + for (moveY in y downTo 1) { + for (x in 0 until WIDTH) { + board[moveY][x] = board[moveY - 1][x] + } + } + // Clear top line + for (x in 0 until WIDTH) { + board[0][x] = false + } + linesCleared++ + } else { + y-- + } + } + + // Increase speed based on lines cleared + if (linesCleared > 0) { + dropInterval = (dropInterval * 0.95).toLong().coerceAtLeast(MIN_DROP_INTERVAL) + } + } + + private fun isLineFull(y: Int): Boolean { + for (x in 0 until WIDTH) { + if (!board[y][x]) return false + } + return true + } + + private fun createRandomPiece(): GamePiece { + return GamePiece(GamePieceType.values().random()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelmintdrop/model/GamePiece.kt b/app/src/main/java/com/pixelmintdrop/model/GamePiece.kt new file mode 100644 index 0000000..37e0268 --- /dev/null +++ b/app/src/main/java/com/pixelmintdrop/model/GamePiece.kt @@ -0,0 +1,48 @@ +package com.pixelmintdrop.model + +/** + * Represents a game piece with its type, position, and rotation + */ +class GamePiece(val type: GamePieceType) { + var x: Int = 0 + var y: Int = 0 + var rotation: Int = 0 + private set + + private val blocks: Array> = type.getBlocks() + + fun getWidth(): Int = blocks[0].size + + fun getHeight(): Int = blocks.size + + fun isBlockAt(x: Int, y: Int): Boolean { + if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight()) return false + return blocks[y][x] + } + + fun rotate() { + rotation = (rotation + 1) % 4 + rotateBlocks() + } + + private fun rotateBlocks() { + val width = getWidth() + val height = getHeight() + val rotated = Array(width) { Array(height) { false } } + + for (y in 0 until height) { + for (x in 0 until width) { + when (rotation) { + 1 -> rotated[x][height - 1 - y] = blocks[y][x] + 2 -> rotated[height - 1 - y][width - 1 - x] = blocks[y][x] + 3 -> rotated[width - 1 - x][y] = blocks[y][x] + else -> rotated[y][x] = blocks[y][x] + } + } + } + + blocks.indices.forEach { i -> + blocks[i] = rotated[i].copyOf() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelmintdrop/model/GamePieceType.kt b/app/src/main/java/com/pixelmintdrop/model/GamePieceType.kt new file mode 100644 index 0000000..17ad868 --- /dev/null +++ b/app/src/main/java/com/pixelmintdrop/model/GamePieceType.kt @@ -0,0 +1,48 @@ +package com.pixelmintdrop.model + +/** + * Represents the different types of game pieces + */ +enum class GamePieceType { + I, J, L, O, S, T, Z; + + fun getBlocks(): Array> { + return when (this) { + I -> arrayOf( + arrayOf(false, false, false, false), + arrayOf(true, true, true, true), + arrayOf(false, false, false, false), + arrayOf(false, false, false, false) + ) + J -> arrayOf( + arrayOf(true, false, false), + arrayOf(true, true, true), + arrayOf(false, false, false) + ) + L -> arrayOf( + arrayOf(false, false, true), + arrayOf(true, true, true), + arrayOf(false, false, false) + ) + O -> arrayOf( + arrayOf(true, true), + arrayOf(true, true) + ) + S -> arrayOf( + arrayOf(false, true, true), + arrayOf(true, true, false), + arrayOf(false, false, false) + ) + T -> arrayOf( + arrayOf(false, true, false), + arrayOf(true, true, true), + arrayOf(false, false, false) + ) + Z -> arrayOf( + arrayOf(true, true, false), + arrayOf(false, true, true), + arrayOf(false, false, false) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mintris/model/HighScore.kt b/app/src/main/java/com/pixelmintdrop/model/HighScore.kt similarity index 79% rename from app/src/main/java/com/mintris/model/HighScore.kt rename to app/src/main/java/com/pixelmintdrop/model/HighScore.kt index b4b6295..ef70516 100644 --- a/app/src/main/java/com/mintris/model/HighScore.kt +++ b/app/src/main/java/com/pixelmintdrop/model/HighScore.kt @@ -1,4 +1,4 @@ -package com.mintris.model +package com.pixelmintdrop.model data class HighScore( val name: String, diff --git a/app/src/main/java/com/mintris/model/HighScoreAdapter.kt b/app/src/main/java/com/pixelmintdrop/model/HighScoreAdapter.kt similarity index 97% rename from app/src/main/java/com/mintris/model/HighScoreAdapter.kt rename to app/src/main/java/com/pixelmintdrop/model/HighScoreAdapter.kt index de4ec52..bcbae60 100644 --- a/app/src/main/java/com/mintris/model/HighScoreAdapter.kt +++ b/app/src/main/java/com/pixelmintdrop/model/HighScoreAdapter.kt @@ -1,4 +1,4 @@ -package com.mintris.model +package com.pixelmintdrop.model import android.graphics.Color import android.view.LayoutInflater @@ -6,7 +6,7 @@ import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.recyclerview.widget.RecyclerView -import com.mintris.R +import com.pixelmintdrop.R class HighScoreAdapter : RecyclerView.Adapter() { private var highScores: List = emptyList() diff --git a/app/src/main/java/com/mintris/model/HighScoreManager.kt b/app/src/main/java/com/pixelmintdrop/model/HighScoreManager.kt similarity index 93% rename from app/src/main/java/com/mintris/model/HighScoreManager.kt rename to app/src/main/java/com/pixelmintdrop/model/HighScoreManager.kt index 6fd7df9..bf35f52 100644 --- a/app/src/main/java/com/mintris/model/HighScoreManager.kt +++ b/app/src/main/java/com/pixelmintdrop/model/HighScoreManager.kt @@ -1,4 +1,4 @@ -package com.mintris.model +package com.pixelmintdrop.model import android.content.Context import android.content.SharedPreferences @@ -12,7 +12,7 @@ class HighScoreManager(private val context: Context) { private val type: Type = object : TypeToken>() {}.type companion object { - private const val PREFS_NAME = "mintris_highscores" + private const val PREFS_NAME = "pixelmintdrop_highscores" private const val KEY_HIGHSCORES = "highscores" private const val MAX_HIGHSCORES = 5 } diff --git a/app/src/main/java/com/mintris/model/PlayerProgressionManager.kt b/app/src/main/java/com/pixelmintdrop/model/PlayerProgressionManager.kt similarity index 89% rename from app/src/main/java/com/mintris/model/PlayerProgressionManager.kt rename to app/src/main/java/com/pixelmintdrop/model/PlayerProgressionManager.kt index a395bb7..6a2e066 100644 --- a/app/src/main/java/com/mintris/model/PlayerProgressionManager.kt +++ b/app/src/main/java/com/pixelmintdrop/model/PlayerProgressionManager.kt @@ -1,8 +1,7 @@ -package com.mintris.model +package com.pixelmintdrop.model import android.content.Context import android.content.SharedPreferences -import com.mintris.R import kotlin.math.pow import kotlin.math.roundToInt import kotlin.math.min @@ -94,24 +93,31 @@ class PlayerProgressionManager(context: Context) { /** * Calculate XP from a game session based on score, lines, level, etc. */ - fun calculateGameXP(score: Int, lines: Int, level: Int, gameTime: Long, - tetrisCount: Int, perfectClearCount: Int): Long { - // Base XP from score with level multiplier (capped at level 10) - val cappedLevel = min(level, 10) - val scoreXP = (score * (1 + LEVEL_MULTIPLIER * cappedLevel)).toLong() - - // XP from lines cleared (reduced for higher levels) - val linesXP = lines * XP_PER_LINE * (1 - (level - 1) * 0.05).coerceAtLeast(0.5) - - // XP from special moves (reduced for higher levels) - val tetrisBonus = tetrisCount * TETRIS_XP_BONUS * (1 - (level - 1) * 0.05).coerceAtLeast(0.5) - val perfectClearBonus = perfectClearCount * PERFECT_CLEAR_XP_BONUS * (1 - (level - 1) * 0.05).coerceAtLeast(0.5) - - // Time bonus (reduced for longer games) - val timeBonus = (gameTime / 60000) * TIME_XP_PER_MINUTE * (1 - (gameTime / 3600000) * 0.1).coerceAtLeast(0.5) - - // Calculate total XP - return (scoreXP + linesXP + tetrisBonus + perfectClearBonus + timeBonus).toLong() + fun calculateGameXP( + score: Int, + lines: Int, + level: Int, + timePlayedMs: Long, + quadCount: Int, + perfectClearCount: Int + ): Long { + // Calculate base XP from score + val scoreXP = score * BASE_SCORE_XP * level + + // Calculate XP from lines cleared + val linesXP = lines * BASE_LINES_XP * level + + // Calculate quad bonus + val quadBonus = quadCount * BASE_QUAD_BONUS * level + + // Calculate perfect clear bonus + val perfectClearBonus = perfectClearCount * BASE_PERFECT_CLEAR_BONUS * level + + // Calculate time bonus (convert ms to seconds) + val timeBonus = (timePlayedMs / 1000.0) * BASE_TIME_XP * level + + // Sum all XP components + return (scoreXP + linesXP + quadBonus + perfectClearBonus + timeBonus).toLong() } /** @@ -292,7 +298,7 @@ class PlayerProgressionManager(context: Context) { } companion object { - private const val PREFS_NAME = "mintris_progression" + private const val PREFS_NAME = "pixelmintdrop_progression" private const val KEY_PLAYER_LEVEL = "player_level" private const val KEY_PLAYER_XP = "player_xp" private const val KEY_TOTAL_XP_EARNED = "total_xp_earned" @@ -328,6 +334,12 @@ class PlayerProgressionManager(context: Context) { THEME_MINIMALIST to 20, THEME_GALAXY to 25 ) + + private const val BASE_SCORE_XP = 0.1 // XP per score point + private const val BASE_LINES_XP = 100.0 // XP per line cleared + private const val BASE_QUAD_BONUS = 500.0 // Bonus XP for clearing 4 lines at once + private const val BASE_PERFECT_CLEAR_BONUS = 1000.0 // Bonus XP for perfect clear + private const val BASE_TIME_XP = 1.0 // XP per second played } /** diff --git a/app/src/main/java/com/pixelmintdrop/model/StatsManager.kt b/app/src/main/java/com/pixelmintdrop/model/StatsManager.kt new file mode 100644 index 0000000..66dcca5 --- /dev/null +++ b/app/src/main/java/com/pixelmintdrop/model/StatsManager.kt @@ -0,0 +1,78 @@ +package com.pixelmintdrop.model + +import android.content.Context +import android.content.SharedPreferences + +/** + * Manages game statistics and high scores + */ +class StatsManager(context: Context) { + companion object { + private const val PREFS_NAME = "game_stats" + private const val KEY_HIGH_SCORE = "high_score" + private const val KEY_TOTAL_GAMES = "total_games" + private const val KEY_TOTAL_LINES = "total_lines" + private const val KEY_TOTAL_QUADS = "total_quads" + private const val KEY_TOTAL_PERFECT_CLEARS = "total_perfect_clears" + private const val KEY_SESSION_SCORE = "session_score" + private const val KEY_SESSION_LINES = "session_lines" + private const val KEY_SESSION_QUADS = "session_quads" + private const val KEY_SESSION_PERFECT_CLEARS = "session_perfect_clears" + } + + private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + + // Session stats + private var sessionScore = 0 + private var sessionLines = 0 + private var sessionQuads = 0 + private var sessionPerfectClears = 0 + + fun getHighScore(): Int = prefs.getInt(KEY_HIGH_SCORE, 0) + fun getTotalGames(): Int = prefs.getInt(KEY_TOTAL_GAMES, 0) + fun getTotalLines(): Int = prefs.getInt(KEY_TOTAL_LINES, 0) + fun getTotalQuads(): Int = prefs.getInt(KEY_TOTAL_QUADS, 0) + fun getTotalPerfectClears(): Int = prefs.getInt(KEY_TOTAL_PERFECT_CLEARS, 0) + + fun getSessionScore(): Int = sessionScore + fun getSessionLines(): Int = sessionLines + fun getSessionQuads(): Int = sessionQuads + fun getSessionPerfectClears(): Int = sessionPerfectClears + + fun updateStats(score: Int, lines: Int, isQuad: Boolean, isPerfectClear: Boolean) { + // Update session stats + sessionScore += score + sessionLines += lines + if (isQuad) sessionQuads++ + if (isPerfectClear) sessionPerfectClears++ + + // Update high score if needed + if (sessionScore > getHighScore()) { + prefs.edit().putInt(KEY_HIGH_SCORE, sessionScore).apply() + } + + // Update total stats + prefs.edit() + .putInt(KEY_TOTAL_LINES, getTotalLines() + lines) + .putInt(KEY_TOTAL_QUADS, getTotalQuads() + if (isQuad) 1 else 0) + .putInt(KEY_TOTAL_PERFECT_CLEARS, getTotalPerfectClears() + if (isPerfectClear) 1 else 0) + .apply() + } + + fun startNewGame() { + // Increment total games counter + prefs.edit().putInt(KEY_TOTAL_GAMES, getTotalGames() + 1).apply() + } + + fun resetSession() { + sessionScore = 0 + sessionLines = 0 + sessionQuads = 0 + sessionPerfectClears = 0 + } + + fun resetAllStats() { + prefs.edit().clear().apply() + resetSession() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mintris/model/Tetromino.kt b/app/src/main/java/com/pixelmintdrop/model/Tetromino.kt similarity index 99% rename from app/src/main/java/com/mintris/model/Tetromino.kt rename to app/src/main/java/com/pixelmintdrop/model/Tetromino.kt index 54bafb2..d0daf97 100644 --- a/app/src/main/java/com/mintris/model/Tetromino.kt +++ b/app/src/main/java/com/pixelmintdrop/model/Tetromino.kt @@ -1,7 +1,7 @@ -package com.mintris.model +package com.pixelmintdrop.model /** - * Represents a Tetris piece (Tetromino) + * Represents a game piece (Tetromino) */ enum class TetrominoType { I, J, L, O, S, T, Z diff --git a/app/src/main/java/com/pixelmintdrop/theme/ThemeManager.kt b/app/src/main/java/com/pixelmintdrop/theme/ThemeManager.kt new file mode 100644 index 0000000..d87a517 --- /dev/null +++ b/app/src/main/java/com/pixelmintdrop/theme/ThemeManager.kt @@ -0,0 +1,52 @@ +package com.pixelmintdrop.theme + +import android.graphics.Color + +object ThemeManager { + // Theme colors + const val COLOR_CLASSIC_BACKGROUND = Color.BLACK + const val COLOR_CLASSIC_FOREGROUND = Color.WHITE + const val COLOR_CLASSIC_ACCENT = Color.CYAN + + const val COLOR_NEON_BACKGROUND = 0xFF0D0221.toInt() + const val COLOR_NEON_FOREGROUND = 0xFFFF00FF.toInt() + const val COLOR_NEON_ACCENT = 0xFF00FFFF.toInt() + + const val COLOR_MONOCHROME_BACKGROUND = 0xFF1A1A1A.toInt() + const val COLOR_MONOCHROME_FOREGROUND = Color.LTGRAY + const val COLOR_MONOCHROME_ACCENT = Color.WHITE + + const val COLOR_RETRO_BACKGROUND = 0xFF3F2832.toInt() + const val COLOR_RETRO_FOREGROUND = 0xFFFF5A5F.toInt() + const val COLOR_RETRO_ACCENT = 0xFFFFB400.toInt() + + const val COLOR_MINIMALIST_BACKGROUND = Color.WHITE + const val COLOR_MINIMALIST_FOREGROUND = Color.BLACK + const val COLOR_MINIMALIST_ACCENT = Color.DKGRAY + + const val COLOR_GALAXY_BACKGROUND = 0xFF0B0C10.toInt() + const val COLOR_GALAXY_FOREGROUND = 0xFF66FCF1.toInt() + const val COLOR_GALAXY_ACCENT = 0xFF45A29E.toInt() + + // Block colors for each piece type + const val COLOR_I_PIECE = 0xFF00F0F0.toInt() + const val COLOR_J_PIECE = 0xFF0000F0.toInt() + const val COLOR_L_PIECE = 0xFFF0A000.toInt() + const val COLOR_O_PIECE = 0xFFF0F000.toInt() + const val COLOR_S_PIECE = 0xFF00F000.toInt() + const val COLOR_T_PIECE = 0xFFA000F0.toInt() + const val COLOR_Z_PIECE = 0xFFF00000.toInt() + + // Ghost piece colors + const val COLOR_GHOST_PIECE = 0x40FFFFFF + const val COLOR_GHOST_PIECE_GLOW = 0x20FFFFFF + + // Grid colors + const val COLOR_GRID_LINE = 0x20FFFFFF + const val COLOR_GRID_BORDER = 0x40FFFFFF + + // Effect colors + const val COLOR_LINE_CLEAR_FLASH = 0x80FFFFFF + const val COLOR_PERFECT_CLEAR_FLASH = 0xFFFFD700.toInt() + const val COLOR_COMBO_FLASH = 0x60FFFFFF +} \ No newline at end of file diff --git a/app/src/main/java/com/mintris/ui/BlockSkinSelector.kt b/app/src/main/java/com/pixelmintdrop/ui/BlockSkinSelector.kt similarity index 99% rename from app/src/main/java/com/mintris/ui/BlockSkinSelector.kt rename to app/src/main/java/com/pixelmintdrop/ui/BlockSkinSelector.kt index d2c7d35..3e1e1c9 100644 --- a/app/src/main/java/com/mintris/ui/BlockSkinSelector.kt +++ b/app/src/main/java/com/pixelmintdrop/ui/BlockSkinSelector.kt @@ -1,4 +1,4 @@ -package com.mintris.ui +package com.pixelmintdrop.ui import android.content.Context import android.graphics.Color @@ -9,9 +9,7 @@ import android.widget.FrameLayout import android.widget.GridLayout import android.widget.TextView import androidx.cardview.widget.CardView -import com.mintris.R -import com.mintris.model.PlayerProgressionManager -import android.animation.ValueAnimator +import com.pixelmintdrop.R import android.graphics.drawable.GradientDrawable /** diff --git a/app/src/main/java/com/mintris/ui/GameUIManager.kt b/app/src/main/java/com/pixelmintdrop/ui/GameUIManager.kt similarity index 95% rename from app/src/main/java/com/mintris/ui/GameUIManager.kt rename to app/src/main/java/com/pixelmintdrop/ui/GameUIManager.kt index 586a13e..41fa22f 100644 --- a/app/src/main/java/com/mintris/ui/GameUIManager.kt +++ b/app/src/main/java/com/pixelmintdrop/ui/GameUIManager.kt @@ -1,14 +1,10 @@ -package com.mintris.ui +package com.pixelmintdrop.ui import android.content.Context import android.graphics.Color import android.view.View -import android.widget.ImageButton -import android.widget.TextView -import com.mintris.R -import com.mintris.databinding.ActivityMainBinding -import com.mintris.model.PlayerProgressionManager -import kotlin.math.roundToInt +import com.pixelmintdrop.databinding.ActivityMainBinding +import com.pixelmintdrop.model.PlayerProgressionManager /** * Handles UI updates and state management for the game interface diff --git a/app/src/main/java/com/mintris/ui/LevelBadge.kt b/app/src/main/java/com/pixelmintdrop/ui/LevelBadge.kt similarity index 98% rename from app/src/main/java/com/mintris/ui/LevelBadge.kt rename to app/src/main/java/com/pixelmintdrop/ui/LevelBadge.kt index 67665e4..3213417 100644 --- a/app/src/main/java/com/mintris/ui/LevelBadge.kt +++ b/app/src/main/java/com/pixelmintdrop/ui/LevelBadge.kt @@ -1,4 +1,4 @@ -package com.mintris.ui +package com.pixelmintdrop.ui import android.content.Context import android.graphics.Canvas diff --git a/app/src/main/java/com/mintris/ui/ProgressionScreen.kt b/app/src/main/java/com/pixelmintdrop/ui/ProgressionScreen.kt similarity index 99% rename from app/src/main/java/com/mintris/ui/ProgressionScreen.kt rename to app/src/main/java/com/pixelmintdrop/ui/ProgressionScreen.kt index 117a440..07d55cc 100644 --- a/app/src/main/java/com/mintris/ui/ProgressionScreen.kt +++ b/app/src/main/java/com/pixelmintdrop/ui/ProgressionScreen.kt @@ -1,4 +1,4 @@ -package com.mintris.ui +package com.pixelmintdrop.ui import android.animation.AnimatorSet import android.animation.ObjectAnimator @@ -12,8 +12,8 @@ import android.view.animation.OvershootInterpolator import android.widget.LinearLayout import android.widget.TextView import androidx.cardview.widget.CardView -import com.mintris.R -import com.mintris.model.PlayerProgressionManager +import com.pixelmintdrop.R +import com.pixelmintdrop.model.PlayerProgressionManager /** * Screen that displays player progression, XP gain, and unlocked rewards diff --git a/app/src/main/java/com/mintris/ui/ThemeSelector.kt b/app/src/main/java/com/pixelmintdrop/ui/ThemeSelector.kt similarity index 99% rename from app/src/main/java/com/mintris/ui/ThemeSelector.kt rename to app/src/main/java/com/pixelmintdrop/ui/ThemeSelector.kt index ece7cd4..059fd6c 100644 --- a/app/src/main/java/com/mintris/ui/ThemeSelector.kt +++ b/app/src/main/java/com/pixelmintdrop/ui/ThemeSelector.kt @@ -1,4 +1,4 @@ -package com.mintris.ui +package com.pixelmintdrop.ui import android.content.Context import android.graphics.Color @@ -9,9 +9,8 @@ import android.widget.FrameLayout import android.widget.GridLayout import android.widget.TextView import androidx.cardview.widget.CardView -import com.mintris.R -import com.mintris.model.PlayerProgressionManager -import android.animation.ValueAnimator +import com.pixelmintdrop.R +import com.pixelmintdrop.model.PlayerProgressionManager import android.graphics.drawable.GradientDrawable import android.util.Log diff --git a/app/src/main/java/com/mintris/ui/XPProgressBar.kt b/app/src/main/java/com/pixelmintdrop/ui/XPProgressBar.kt similarity index 98% rename from app/src/main/java/com/mintris/ui/XPProgressBar.kt rename to app/src/main/java/com/pixelmintdrop/ui/XPProgressBar.kt index 930da2f..f752e75 100644 --- a/app/src/main/java/com/mintris/ui/XPProgressBar.kt +++ b/app/src/main/java/com/pixelmintdrop/ui/XPProgressBar.kt @@ -1,4 +1,4 @@ -package com.mintris.ui +package com.pixelmintdrop.ui import android.animation.ValueAnimator import android.content.Context @@ -9,8 +9,6 @@ import android.graphics.RectF import android.util.AttributeSet import android.view.View import android.view.animation.AccelerateDecelerateInterpolator -import androidx.core.content.ContextCompat -import com.mintris.R /** * Custom progress bar for displaying player XP with animation capabilities diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 5591e04..71bd239 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -5,7 +5,7 @@ android:viewportWidth="108" android:viewportHeight="108"> - + @@ -19,7 +19,7 @@ android:fillColor="#FFFFFF" android:pathData="M48,48h12v12h-12z" /> - + diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml index f4a4d50..eb8a729 100644 --- a/app/src/main/res/layout-land/activity_main.xml +++ b/app/src/main/res/layout-land/activity_main.xml @@ -28,7 +28,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> - @@ -72,7 +72,7 @@ app:layout_constraintEnd_toEndOf="parent" android:layout_marginTop="24dp"/> - - - - - - - @@ -683,7 +683,7 @@ android:paddingBottom="32dp"> - - - @@ -33,7 +33,7 @@ - - @@ -136,7 +136,7 @@ - - - @@ -567,7 +567,7 @@ android:textAllCaps="false" android:singleLine="true" /> - @@ -592,14 +592,14 @@ android:paddingBottom="32dp"> - - - - mintris + Pixel Mint Drop game over score level @@ -44,7 +44,7 @@ singles: %d doubles: %d triples: %d - tetrises: %d + quads: %d reset stats music Customization diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index ea37314..a280be0 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,7 +1,7 @@ - -