diff --git a/README.md b/README.md index 5bd8440..15b03d7 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# pixelmintdrop +# Mintris -A modern block-stacking puzzle game for Android, featuring smooth animations, responsive controls, and a beautiful minimalist design. +A modern Tetris implementation for Android, featuring smooth animations, responsive controls, and a beautiful minimalist design. ## Features ### Core Gameplay -- Classic block-stacking mechanics +- Classic Tetris 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 -- Quad (4 lines): 1200 points +- Tetris (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 Quad** - - 50% bonus (1.5x) for consecutive quad clears - - Resets if a non-quad clear is performed +3. **Back-to-Back Tetris** + - 50% bonus (1.5x) for consecutive Tetris clears + - Resets if a non-Tetris clear is performed 4. **Perfect Clear** - 2x for single line - 3x for double - 4x for triple - - 5x for quad + - 5x for Tetris - Awarded when clearing lines without leaving blocks 5. **All Clear** @@ -102,124 +102,11 @@ The game features a comprehensive scoring system: - Follows Material Design guidelines - Implements high score persistence using SharedPreferences -## Project Improvements and Best Practices - -### Performance Optimizations - -The codebase includes several performance optimizations: - -1. **Release Build Configuration** - - Minification enabled to reduce APK size - - Resource shrinking to remove unused resources - - ProGuard rules to optimize while preserving critical classes - -2. **Memory Management** - - Proper lifecycle handling to prevent memory leaks - - Resource cleanup through `releaseResources()` methods - - Efficient bitmap handling with reuse when possible - -3. **Rendering Efficiency** - - Custom view invalidation limited to areas that need updating - - Hardware acceleration for canvas operations - - Bitmap caching for frequently used graphics - -### Code Organization - -The codebase follows good architecture practices: - -1. **Package Structure** - - `model`: Data classes and game logic - - `game`: Core gameplay implementation - - `ui`: User interface components - - `audio`: Sound and music management - - `accessibility`: Accessibility helpers - -2. **Responsibility Separation** - - `GameLifecycleManager`: Handles lifecycle events - - `GameUIManager`: Manages UI state and updates - - `GameAccessibilityHelper`: Improves accessibility features - - `GamepadController`: Manages gamepad input - -### Google Play Compliance - -The app meets Google Play standards: - -1. **Manifest Configuration** - - Proper permissions declaration - - Screen orientation handling - - Full backup rules for user data - -2. **Accessibility Support** - - Content descriptions for UI elements - - Color contrast considerations - - Screen reader compatibility - -3. **Shortcuts and Deep Links** - - App shortcuts for quick actions - - Proper intent handling - -### Usage Examples - -#### Lifecycle Management - -```kotlin -// In your activity -private lateinit var lifecycleManager: GameLifecycleManager - -override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - lifecycleManager = GameLifecycleManager(this) -} - -override fun onPause() { - super.onPause() - lifecycleManager.onPause(gameView, gameMusic, statsManager, highScoreManager) -} - -override fun onResume() { - super.onResume() - lifecycleManager.onResume(gameView, gameMusic, isMusicEnabled) -} - -override fun onDestroy() { - lifecycleManager.onDestroy(gameView, gameMusic, statsManager, highScoreManager) - super.onDestroy() -} -``` - -#### Accessibility Implementation - -```kotlin -// In your activity -private lateinit var accessibilityHelper: GameAccessibilityHelper - -override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - accessibilityHelper = GameAccessibilityHelper(this) - - // Setup accessibility descriptions for controls - accessibilityHelper.setupAccessibilityDescriptions( - leftButton, rightButton, rotateButton, dropButton, - holdButton, pauseButton, gameView, holdPieceView, nextPieceView - ) -} - -// When level changes -private fun onLevelUp(newLevel: Int) { - accessibilityHelper.announceLevelUp(gameView, newLevel) -} - -// When game ends -private fun onGameOver(score: Long) { - accessibilityHelper.announceGameOver(gameView, score) -} -``` - ## Building from Source 1. Clone the repository: ```bash -git clone https://github.com/cmclark00/pixelmintdrop.git +git clone https://github.com/cmclark00/mintris.git ``` 2. Open the project in Android Studio diff --git a/app/build.gradle b/app/build.gradle index 45c83db..da55975 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,13 +4,13 @@ plugins { } android { - namespace "com.pixelmintdrop" - compileSdk 35 + namespace 'com.mintris' + compileSdk 34 defaultConfig { - applicationId "com.pixelmintdrop" + applicationId "com.mintris" minSdk 30 - targetSdk 35 + targetSdk 34 versionCode 1 versionName "1.0" @@ -19,15 +19,13 @@ android { buildTypes { release { - minifyEnabled true - shrinkResources true + minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } buildFeatures { viewBinding true - dataBinding true } compileOptions { @@ -40,15 +38,6 @@ android { } } -// Enable strict mode for debug builds -android.applicationVariants.all { variant -> - if (variant.buildType.name == "debug") { - variant.mergedFlavor.manifestPlaceholders = [enableStrictMode: "true"] - } else { - variant.mergedFlavor.manifestPlaceholders = [enableStrictMode: "false"] - } -} - dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.12.0' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 461f647..e64cabd 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -6,40 +6,7 @@ # http://developer.android.com/guide/developing/tools/proguard.html # Keep models intact --keep class com.pixelmintdrop.model.** { *; } - -# Keep game classes intact to prevent issues --keep class com.pixelmintdrop.game.** { *; } - -# Preserve critical classes that might be used through reflection --keep class com.pixelmintdrop.audio.GameMusic { *; } --keep class com.pixelmintdrop.ui.** { *; } - -# Keep all public methods in the MainActivity --keepclassmembers class com.pixelmintdrop.MainActivity { - public *; -} - -# Keep serializable and parcelable classes for proper game state saving --keepnames class * implements java.io.Serializable --keepclassmembers class * implements java.io.Serializable { - static final long serialVersionUID; - private static final java.io.ObjectStreamField[] serialPersistentFields; - !static !transient ; - private void writeObject(java.io.ObjectOutputStream); - private void readObject(java.io.ObjectInputStream); - java.lang.Object writeReplace(); - java.lang.Object readResolve(); -} - -# Preserve line number information for debugging stack traces --keepattributes SourceFile,LineNumberTable - -# Keep Gson usage intact --keep class com.google.gson.** { *; } --keep class * implements com.google.gson.TypeAdapterFactory --keep class * implements com.google.gson.JsonSerializer --keep class * implements com.google.gson.JsonDeserializer +-keep class com.mintris.model.** { *; } # Uncomment this to preserve the line number information for # debugging stack traces. diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0d57748..8815afb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,46 +7,37 @@ + android:theme="@style/Theme.Mintris"> + android:excludeFromRecents="false"> - + android:exported="false" /> \ No newline at end of file diff --git a/app/src/main/java/com/pixelmintdrop/HighScoreEntryActivity.kt b/app/src/main/java/com/mintris/HighScoreEntryActivity.kt similarity index 93% rename from app/src/main/java/com/pixelmintdrop/HighScoreEntryActivity.kt rename to app/src/main/java/com/mintris/HighScoreEntryActivity.kt index 09e2064..339c3b6 100644 --- a/app/src/main/java/com/pixelmintdrop/HighScoreEntryActivity.kt +++ b/app/src/main/java/com/mintris/HighScoreEntryActivity.kt @@ -1,12 +1,16 @@ -package com.pixelmintdrop +package com.mintris 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.pixelmintdrop.databinding.HighScoreEntryBinding -import com.pixelmintdrop.model.HighScore -import com.pixelmintdrop.model.HighScoreManager -import com.pixelmintdrop.model.PlayerProgressionManager +import com.mintris.databinding.HighScoreEntryBinding +import com.mintris.model.HighScore +import com.mintris.model.HighScoreManager +import com.mintris.model.PlayerProgressionManager import android.graphics.Color import android.view.KeyEvent import android.view.InputDevice diff --git a/app/src/main/java/com/pixelmintdrop/HighScoresActivity.kt b/app/src/main/java/com/mintris/HighScoresActivity.kt similarity index 94% rename from app/src/main/java/com/pixelmintdrop/HighScoresActivity.kt rename to app/src/main/java/com/mintris/HighScoresActivity.kt index ea21392..0fdc75d 100644 --- a/app/src/main/java/com/pixelmintdrop/HighScoresActivity.kt +++ b/app/src/main/java/com/mintris/HighScoresActivity.kt @@ -1,12 +1,15 @@ -package com.pixelmintdrop +package com.mintris import android.os.Bundle +import android.widget.Button +import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.LinearLayoutManager -import com.pixelmintdrop.databinding.HighScoresBinding -import com.pixelmintdrop.model.HighScoreAdapter -import com.pixelmintdrop.model.HighScoreManager -import com.pixelmintdrop.model.PlayerProgressionManager +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 android.graphics.Color import android.util.Log import android.view.KeyEvent diff --git a/app/src/main/java/com/pixelmintdrop/MainActivity.kt b/app/src/main/java/com/mintris/MainActivity.kt similarity index 97% rename from app/src/main/java/com/pixelmintdrop/MainActivity.kt rename to app/src/main/java/com/mintris/MainActivity.kt index 9384137..22f2ea8 100644 --- a/app/src/main/java/com/pixelmintdrop/MainActivity.kt +++ b/app/src/main/java/com/mintris/MainActivity.kt @@ -1,4 +1,4 @@ -package com.pixelmintdrop +package com.mintris 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.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 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 java.text.SimpleDateFormat import java.util.* import android.graphics.Color @@ -32,11 +32,12 @@ import android.view.KeyEvent import android.os.Handler import android.os.Looper import android.view.MotionEvent -import com.pixelmintdrop.game.GamepadController +import com.mintris.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 @@ -152,7 +153,7 @@ class MainActivity : AppCompatActivity(), pauseMenuScrollView = binding.pauseMenuScrollView // Load random mode setting - isRandomModeEnabled = getSharedPreferences("com.com.pixelmintgames.pixelmintdrop.preferences", Context.MODE_PRIVATE) + isRandomModeEnabled = getSharedPreferences("com.mintris.preferences", Context.MODE_PRIVATE) .getBoolean("random_mode_enabled", false) // Initialize gamepad controller @@ -301,14 +302,7 @@ class MainActivity : AppCompatActivity(), statsManager.updateSessionStats(finalScore, gameBoard.lines, piecesPlaced, timePlayedMs, currentLevel) // Handle progression - XP earned, potential level up - val xpGained = progressionManager.calculateGameXP( - score = finalScore, - lines = gameBoard.lines, - level = currentLevel, - timePlayedMs = timePlayedMs, - quadCount = statsManager.getSessionQuads(), - perfectClearCount = statsManager.getSessionPerfectClears() - ) + val xpGained = progressionManager.calculateGameXP(finalScore, gameBoard.lines, currentLevel, timePlayedMs, statsManager.getSessionTetrises(), 0) val newRewards = progressionManager.addXP(xpGained) // Show progression screen if player earned XP @@ -478,9 +472,9 @@ class MainActivity : AppCompatActivity(), score = score, lines = gameBoard.lines, level = currentLevel, - timePlayedMs = gameTime, - quadCount = statsManager.getSessionQuads(), - perfectClearCount = statsManager.getSessionPerfectClears() + gameTime = gameTime, + tetrisCount = statsManager.getSessionTetrises(), + perfectClearCount = 0 // Implement perfect clear tracking if needed ) // Add XP and check for rewards @@ -503,7 +497,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.sessionQuadsText.text = getString(R.string.quads, statsManager.getSessionQuads()) + binding.sessionTetrisesText.text = getString(R.string.tetrises, statsManager.getSessionTetrises()) // Flag to track if high score screen will be shown var showingHighScore = false @@ -630,7 +624,7 @@ class MainActivity : AppCompatActivity(), isEnabled = progressionManager.getPlayerLevel() >= 5 setOnCheckedChangeListener { _, isChecked -> isRandomModeEnabled = isChecked - getSharedPreferences("com.com.pixelmintgames.pixelmintdrop.preferences", Context.MODE_PRIVATE) + getSharedPreferences("com.mintris.preferences", Context.MODE_PRIVATE) .edit() .putBoolean("random_mode_enabled", isChecked) .apply() @@ -918,14 +912,7 @@ class MainActivity : AppCompatActivity(), statsManager.updateSessionStats(finalScore, gameBoard.lines, piecesPlaced, timePlayedMs, currentLevel) // Handle progression - XP earned, potential level up - val xpGained = progressionManager.calculateGameXP( - score = finalScore, - lines = gameBoard.lines, - level = currentLevel, - timePlayedMs = timePlayedMs, - quadCount = statsManager.getSessionQuads(), - perfectClearCount = statsManager.getSessionPerfectClears() - ) + val xpGained = progressionManager.calculateGameXP(finalScore, gameBoard.lines, currentLevel, timePlayedMs, statsManager.getSessionTetrises(), 0) val newRewards = progressionManager.addXP(xpGained) // Show progression screen if player earned XP @@ -1224,7 +1211,7 @@ class MainActivity : AppCompatActivity(), * Check if user has seen the gamepad help */ private fun hasSeenGamepadHelp(): Boolean { - val prefs = getSharedPreferences("com.com.pixelmintgames.pixelmintdrop.preferences", Context.MODE_PRIVATE) + val prefs = getSharedPreferences("com.mintris.preferences", Context.MODE_PRIVATE) return prefs.getBoolean("has_seen_gamepad_help", false) } @@ -1232,7 +1219,7 @@ class MainActivity : AppCompatActivity(), * Mark that user has seen the gamepad help */ private fun markGamepadHelpSeen() { - val prefs = getSharedPreferences("com.com.pixelmintgames.pixelmintdrop.preferences", Context.MODE_PRIVATE) + val prefs = getSharedPreferences("com.mintris.preferences", Context.MODE_PRIVATE) prefs.edit().putBoolean("has_seen_gamepad_help", true).apply() } diff --git a/app/src/main/java/com/pixelmintdrop/StatsActivity.kt b/app/src/main/java/com/mintris/StatsActivity.kt similarity index 93% rename from app/src/main/java/com/pixelmintdrop/StatsActivity.kt rename to app/src/main/java/com/mintris/StatsActivity.kt index 1652365..c7d2e0e 100644 --- a/app/src/main/java/com/pixelmintdrop/StatsActivity.kt +++ b/app/src/main/java/com/mintris/StatsActivity.kt @@ -1,11 +1,13 @@ -package com.pixelmintdrop +package com.mintris import android.os.Bundle +import android.widget.Button +import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import com.pixelmintdrop.databinding.ActivityStatsBinding -import com.pixelmintdrop.model.StatsManager -import com.pixelmintdrop.model.PlayerProgressionManager +import com.mintris.databinding.ActivityStatsBinding +import com.mintris.model.StatsManager +import com.mintris.model.PlayerProgressionManager import android.graphics.Color import java.text.SimpleDateFormat import java.util.* @@ -78,7 +80,7 @@ class StatsActivity : AppCompatActivity() { binding.totalSinglesText.setTextColor(textColor) binding.totalDoublesText.setTextColor(textColor) binding.totalTriplesText.setTextColor(textColor) - binding.totalQuadsText.setTextColor(textColor) + binding.totalTetrisesText.setTextColor(textColor) binding.maxLevelText.setTextColor(textColor) binding.maxScoreText.setTextColor(textColor) binding.maxLinesText.setTextColor(textColor) @@ -116,7 +118,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.totalQuadsText.text = getString(R.string.quads, statsManager.getTotalQuads()) + binding.totalTetrisesText.text = getString(R.string.tetrises, statsManager.getTotalTetrises()) // Update best performance stats binding.maxLevelText.text = getString(R.string.max_level, statsManager.getMaxLevel()) diff --git a/app/src/main/java/com/pixelmintdrop/ThemeManager.kt b/app/src/main/java/com/mintris/ThemeManager.kt similarity index 92% rename from app/src/main/java/com/pixelmintdrop/ThemeManager.kt rename to app/src/main/java/com/mintris/ThemeManager.kt index b2d1e1f..56ce224 100644 --- a/app/src/main/java/com/pixelmintdrop/ThemeManager.kt +++ b/app/src/main/java/com/mintris/ThemeManager.kt @@ -1,4 +1,4 @@ -package com.pixelmintdrop +package com.mintris import android.graphics.Color diff --git a/app/src/main/java/com/pixelmintdrop/audio/GameMusic.kt b/app/src/main/java/com/mintris/audio/GameMusic.kt similarity index 91% rename from app/src/main/java/com/pixelmintdrop/audio/GameMusic.kt rename to app/src/main/java/com/mintris/audio/GameMusic.kt index 38a8615..32c2f6e 100644 --- a/app/src/main/java/com/pixelmintdrop/audio/GameMusic.kt +++ b/app/src/main/java/com/mintris/audio/GameMusic.kt @@ -1,11 +1,11 @@ -package com.pixelmintdrop.audio +package com.mintris.audio import android.content.Context import android.media.MediaPlayer import android.media.AudioAttributes import android.os.Build import android.util.Log -import com.pixelmintdrop.R +import com.mintris.R class GameMusic(private val context: Context) { private var mediaPlayer: MediaPlayer? = null @@ -148,25 +148,16 @@ class GameMusic(private val context: Context) { fun isEnabled(): Boolean = isEnabled - /** - * Releases all media player resources to prevent memory leaks - */ - fun releaseResources() { + fun release() { try { - Log.d("GameMusic", "Releasing MediaPlayer resources") + Log.d("GameMusic", "Releasing MediaPlayer") mediaPlayer?.release() gameOverPlayer?.release() mediaPlayer = null gameOverPlayer = null isPrepared = false } catch (e: Exception) { - Log.e("GameMusic", "Error releasing music resources: ${e.message}") + Log.e("GameMusic", "Error releasing music: ${e.message}") } } - - // Keeping old method for backward compatibility - @Deprecated("Use releaseResources() instead", ReplaceWith("releaseResources()")) - fun release() { - releaseResources() - } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelmintdrop/game/GameHaptics.kt b/app/src/main/java/com/mintris/game/GameHaptics.kt similarity index 97% rename from app/src/main/java/com/pixelmintdrop/game/GameHaptics.kt rename to app/src/main/java/com/mintris/game/GameHaptics.kt index 7629065..731ac8b 100644 --- a/app/src/main/java/com/pixelmintdrop/game/GameHaptics.kt +++ b/app/src/main/java/com/mintris/game/GameHaptics.kt @@ -1,4 +1,4 @@ -package com.pixelmintdrop.game +package com.mintris.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() // Quad: longest vibration + 4 -> (200L * multiplier).toLong() // Tetris: 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 // Quad: maximum vibration + 4 -> 255 // Tetris: maximum vibration else -> (80 * multiplier).toInt().coerceAtMost(255) } diff --git a/app/src/main/java/com/pixelmintdrop/game/GameView.kt b/app/src/main/java/com/mintris/game/GameView.kt similarity index 57% rename from app/src/main/java/com/pixelmintdrop/game/GameView.kt rename to app/src/main/java/com/mintris/game/GameView.kt index 2a062ee..a89ad77 100644 --- a/app/src/main/java/com/pixelmintdrop/game/GameView.kt +++ b/app/src/main/java/com/mintris/game/GameView.kt @@ -1,4 +1,4 @@ -package com.pixelmintdrop.game +package com.mintris.game import android.animation.ValueAnimator import android.content.Context @@ -20,19 +20,14 @@ import android.view.View import android.view.animation.LinearInterpolator import android.hardware.display.DisplayManager import android.view.Display -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 com.mintris.model.GameBoard +import com.mintris.model.Tetromino +import com.mintris.model.TetrominoType import kotlin.math.abs +import kotlin.math.min /** - * GameView that renders the block-stacking game and handles touch input + * GameView that renders the Tetris game and handles touch input */ class GameView @JvmOverloads constructor( context: Context, @@ -50,13 +45,9 @@ class GameView @JvmOverloads constructor( // Game state private var isRunning = false - var isPaused = false + var isPaused = false // Changed from private to public to allow access from MainActivity private var score = 0 - // Current piece - private var currentPiece: GamePiece? = null - private var nextPiece: GamePiece? = null - // Callbacks var onNextPieceChanged: (() -> Unit)? = null @@ -77,22 +68,22 @@ class GameView @JvmOverloads constructor( private val ghostBlockPaint = Paint().apply { color = Color.WHITE - alpha = 80 + alpha = 80 // 30% opacity isAntiAlias = true } private val gridPaint = Paint().apply { - color = Color.parseColor("#222222") - alpha = 20 + color = Color.parseColor("#222222") // Very dark gray + alpha = 20 // Reduced from 40 to be more subtle isAntiAlias = true strokeWidth = 1f style = Paint.Style.STROKE - maskFilter = null + maskFilter = null // Ensure no blur effect on grid lines } private val glowPaint = Paint().apply { color = Color.WHITE - alpha = 40 + alpha = 40 // Reduced from 80 for more subtlety isAntiAlias = true style = Paint.Style.STROKE strokeWidth = 1.5f @@ -107,20 +98,24 @@ 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) + maskFilter = BlurMaskFilter(32f, BlurMaskFilter.Blur.OUTER) // Increased from 16f to 32f } + // 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() { @@ -132,6 +127,7 @@ class GameView @JvmOverloads constructor( } } + // Touch parameters private var lastTouchX = 0f private var lastTouchY = 0f private var startX = 0f @@ -220,7 +216,7 @@ class GameView @JvmOverloads constructor( pause() // Load saved block skin - val prefs = context.getSharedPreferences("pixelmintdrop_progression", Context.MODE_PRIVATE) + val prefs = context.getSharedPreferences("mintris_progression", Context.MODE_PRIVATE) currentBlockSkin = prefs.getString("selected_block_skin", "block_skin_1") ?: "block_skin_1" // Connect our callbacks to the GameBoard @@ -333,7 +329,7 @@ class GameView @JvmOverloads constructor( } // Save the selection to SharedPreferences - val prefs = context.getSharedPreferences("pixelmintdrop_progression", Context.MODE_PRIVATE) + val prefs = context.getSharedPreferences("mintris_progression", Context.MODE_PRIVATE) prefs.edit().putString("selected_block_skin", skinId).commit() // Force a refresh of the view @@ -432,29 +428,197 @@ class GameView @JvmOverloads constructor( override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) - // Calculate block size based on view dimensions and board size - blockSize = minOf( - w / (gameBoard.width + 2f), // Add padding - h / (gameBoard.height + 2f) // Add padding - ) + // Force hardware acceleration - Critical for performance + setLayerType(LAYER_TYPE_HARDWARE, null) - // Center the board - boardLeft = (w - gameBoard.width * blockSize) / 2f - boardTop = (h - gameBoard.height * blockSize) / 2f + // 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") } 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 the game board - drawBoard(canvas) + // Draw background (already black from theme) - // Draw ghost piece - drawGhostPiece(canvas) + // Draw board border glow + drawBoardBorder(canvas) - // Draw current piece - currentPiece?.let { piece -> - drawPiece(canvas, piece) + // 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() + } } } @@ -612,54 +776,221 @@ class GameView @JvmOverloads constructor( * Draw the ghost piece (landing preview) */ private fun drawGhostPiece(canvas: Canvas) { - 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 - } - } - - 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() + val piece = gameBoard.getCurrentPiece() ?: return + val ghostY = gameBoard.getGhostY() - blockPaint.alpha = alpha - for (y in 0 until height) { - for (x in 0 until width) { + // 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 left = offsetX + (piece.x + x) * blockSize - val top = offsetY + (piece.y + y) * blockSize - val right = left + blockSize - val bottom = top + blockSize + val boardX = piece.x + x + val boardY = ghostY + y - // 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) + 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 + ) + } } } } } + /** + * 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 + + // 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) + } + } + } + + // 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() + } + /** * Check if the given board position is part of the current piece */ @@ -695,7 +1026,16 @@ class GameView @JvmOverloads constructor( // Custom touch event handling override fun onTouchEvent(event: MotionEvent): Boolean { - if (!isRunning || isPaused) return false + 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 + } when (event.action) { MotionEvent.ACTION_DOWN -> { @@ -703,53 +1043,120 @@ class GameView @JvmOverloads constructor( startY = event.y lastTouchX = event.x lastTouchY = event.y - lastTapTime = System.currentTimeMillis() - return true + lastTapTime = currentTime // Set the tap time when touch starts + + // Reset direction lock + lockedDirection = null } MotionEvent.ACTION_MOVE -> { - val dx = event.x - lastTouchX - val dy = event.y - lastTouchY + val deltaX = event.x - lastTouchX + val deltaY = event.y - lastTouchY - // Horizontal movement - if (abs(dx) > blockSize / 2) { - if (dx > 0) { - movePieceRight() - } else { - movePieceLeft() + // 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 + } } - lastTouchX = event.x } - // Vertical movement (soft drop) - if (dy > blockSize / 2) { - dropPiece() - lastTouchY = event.y + // 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 + } } - - return true } MotionEvent.ACTION_UP -> { - val dx = event.x - startX - val dy = event.y - startY - val tapTime = System.currentTimeMillis() - lastTapTime + val deltaX = event.x - startX + val deltaY = event.y - startY + val moveTime = currentTime - lastTapTime - // Detect tap for rotation - if (abs(dx) < blockSize / 2 && abs(dy) < blockSize / 2 && tapTime < 200) { - rotatePiece() + // 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 swipe down for hard drop - if (dy > height / 4) { - hardDrop() + // 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() } - return true + // Reset direction lock + lockedDirection = null } } - return false + return true } /** @@ -775,7 +1182,7 @@ class GameView @JvmOverloads constructor( /** * Get the next piece that will be spawned */ - fun getNextPiece(): GamePiece? { + fun getNextPiece(): Tetromino? { return gameBoard.getNextPiece() } @@ -784,46 +1191,12 @@ class GameView @JvmOverloads constructor( */ fun getGameBoard(): GameBoard = gameBoard - /** - * Get the current piece type (for accessibility) - */ - fun getCurrentPieceType(): TetrominoType? { - return gameBoard.getCurrentPiece()?.type - } - /** * Clean up resources when view is detached */ override fun onDetachedFromWindow() { super.onDetachedFromWindow() handler.removeCallbacks(gameLoopRunnable) - releaseResources() - } - - /** - * Release resources to prevent memory leaks - */ - fun releaseResources() { - // Stop all animations - pulseAnimator?.cancel() - pulseAnimator?.removeAllUpdateListeners() - pulseAnimator = null - - // Stop game over animation - gameOverAnimator?.cancel() - gameOverAnimator?.removeAllUpdateListeners() - gameOverAnimator = null - - // Remove callbacks - handler.removeCallbacksAndMessages(null) - - // Clean up references - gameHaptics = null - onGameOver = null - onGameStateChanged = null - onPieceMove = null - onPieceLock = null - onLineClear = null } /** @@ -1135,103 +1508,4 @@ 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/pixelmintdrop/game/GamepadController.kt b/app/src/main/java/com/mintris/game/GamepadController.kt similarity index 98% rename from app/src/main/java/com/pixelmintdrop/game/GamepadController.kt rename to app/src/main/java/com/mintris/game/GamepadController.kt index 83718b2..906758d 100644 --- a/app/src/main/java/com/pixelmintdrop/game/GamepadController.kt +++ b/app/src/main/java/com/mintris/game/GamepadController.kt @@ -1,4 +1,4 @@ -package com.pixelmintdrop.game +package com.mintris.game import android.os.SystemClock import android.view.InputDevice @@ -8,11 +8,13 @@ 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 pixelmintdrop game. + * GamepadController handles gamepad input for the Mintris game. * Supports multiple gamepad types including: * - Microsoft Xbox controllers * - Sony PlayStation controllers @@ -256,7 +258,7 @@ class GamepadController( 1 -> 100 2 -> 150 3 -> 200 - else -> 255 // For quad (4 lines) + else -> 255 // For tetris (4 lines) } vibrateGamepad(RUMBLE_LINE_CLEAR_DURATION_MS, amplitude) } diff --git a/app/src/main/java/com/pixelmintdrop/game/HoldPieceView.kt b/app/src/main/java/com/mintris/game/HoldPieceView.kt similarity index 97% rename from app/src/main/java/com/pixelmintdrop/game/HoldPieceView.kt rename to app/src/main/java/com/mintris/game/HoldPieceView.kt index ae7272c..6f51022 100644 --- a/app/src/main/java/com/pixelmintdrop/game/HoldPieceView.kt +++ b/app/src/main/java/com/mintris/game/HoldPieceView.kt @@ -1,4 +1,4 @@ -package com.pixelmintdrop.game +package com.mintris.game import android.content.Context import android.graphics.BlurMaskFilter @@ -7,7 +7,8 @@ import android.graphics.Color import android.graphics.Paint import android.util.AttributeSet import android.view.View -import com.pixelmintdrop.model.GameBoard +import com.mintris.model.GameBoard +import com.mintris.model.Tetromino import kotlin.math.min /** diff --git a/app/src/main/java/com/mintris/game/NextPieceView.kt b/app/src/main/java/com/mintris/game/NextPieceView.kt new file mode 100644 index 0000000..c82fe2c --- /dev/null +++ b/app/src/main/java/com/mintris/game/NextPieceView.kt @@ -0,0 +1,99 @@ +package com.mintris.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 Tetromino 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?.let { + it.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 + 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/pixelmintdrop/game/TitleScreen.kt b/app/src/main/java/com/mintris/game/TitleScreen.kt similarity index 80% rename from app/src/main/java/com/pixelmintdrop/game/TitleScreen.kt rename to app/src/main/java/com/mintris/game/TitleScreen.kt index f9baa6d..324f81b 100644 --- a/app/src/main/java/com/pixelmintdrop/game/TitleScreen.kt +++ b/app/src/main/java/com/mintris/game/TitleScreen.kt @@ -1,4 +1,4 @@ -package com.pixelmintdrop.game +package com.mintris.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.pixelmintdrop.model.HighScoreManager -import com.pixelmintdrop.model.HighScore -import com.pixelmintdrop.model.PlayerProgressionManager +import com.mintris.model.HighScoreManager +import com.mintris.model.HighScore +import com.mintris.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 piecesToAdd = mutableListOf() + private val tetrominosToAdd = 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 piece shapes (I, O, T, S, Z, J, L) - private val pieceShapes = arrayOf( + // Define tetromino shapes (I, O, T, S, Z, J, L) + private val tetrominoShapes = arrayOf( // I arrayOf( intArrayOf(0, 0, 0, 0), @@ -96,8 +96,8 @@ class TitleScreen @JvmOverloads constructor( ) ) - // FallingPiece class to represent falling pieces - private class FallingPiece( + // Tetromino class to represent falling pieces + private class Tetromino( var x: Float, var y: Float, val shape: Array, @@ -106,7 +106,7 @@ class TitleScreen @JvmOverloads constructor( val rotation: Int = 0 ) - private val pieces = mutableListOf() + private val tetrominos = mutableListOf() init { // Title text settings @@ -138,14 +138,14 @@ class TitleScreen @JvmOverloads constructor( alpha = 200 } - // General paint settings for pieces + // General paint settings for tetrominos paint.apply { color = themeColor style = Paint.Style.FILL isAntiAlias = true } - // Glow paint settings for pieces + // Glow paint settings for tetrominos glowPaint.apply { color = themeColor style = Paint.Style.FILL @@ -159,26 +159,26 @@ class TitleScreen @JvmOverloads constructor( width = w height = h - // Clear existing pieces - pieces.clear() + // Clear existing tetrominos + tetrominos.clear() - // Initialize some pieces + // Initialize some tetrominos repeat(20) { - val piece = createRandomPiece() - pieces.add(piece) + val tetromino = createRandomTetromino() + tetrominos.add(tetromino) } } - private fun createRandomPiece(): FallingPiece { + private fun createRandomTetromino(): Tetromino { val x = random.nextFloat() * (width - 150) + 50 // Keep away from edges val y = -cellSize * 4 - (random.nextFloat() * height / 2) - val shapeIndex = random.nextInt(pieceShapes.size) - val shape = pieceShapes[shapeIndex] + val shapeIndex = random.nextInt(tetrominoShapes.size) + val shape = tetrominoShapes[shapeIndex] val speed = 1f + random.nextFloat() * 2f val scale = 0.8f + random.nextFloat() * 0.4f val rotation = random.nextInt(4) * 90 - return FallingPiece(x, y, shape, speed, scale, rotation) + return Tetromino(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 pieces - pieces.addAll(piecesToAdd) - piecesToAdd.clear() + // Add any pending tetrominos + tetrominos.addAll(tetrominosToAdd) + tetrominosToAdd.clear() - // Update and draw falling pieces - val piecesToRemove = mutableListOf() + // Update and draw falling tetrominos + val tetrominosToRemove = mutableListOf() - for (piece in pieces) { - piece.y += piece.speed + for (tetromino in tetrominos) { + tetromino.y += tetromino.speed - // Remove pieces that have fallen off the screen - if (piece.y > height) { - piecesToRemove.add(piece) - piecesToAdd.add(createRandomPiece()) + // Remove tetrominos that have fallen off the screen + if (tetromino.y > height) { + tetrominosToRemove.add(tetromino) + tetrominosToAdd.add(createRandomTetromino()) } else { try { - // 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) { + // 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) { val left = x * cellSize val top = y * cellSize val right = left + cellSize val bottom = top + cellSize // Draw block with glow effect - 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) { + canvas.withTranslation(tetromino.x, tetromino.y) { + withScale(tetromino.scale, tetromino.scale) { + withRotation(tetromino.rotation.toFloat(), + tetromino.shape.size * cellSize / 2, + tetromino.shape.size * cellSize / 2) { // Draw glow canvas.drawRect(left - 8f, top - 8f, right + 8f, bottom + 8f, glowPaint) // Draw block @@ -230,17 +230,17 @@ class TitleScreen @JvmOverloads constructor( } } } catch (e: Exception) { - Log.e("TitleScreen", "Error drawing piece", e) + Log.e("TitleScreen", "Error drawing tetromino", e) } } } - // Remove pieces that fell off the screen - pieces.removeAll(piecesToRemove) + // Remove tetrominos that fell off the screen + tetrominos.removeAll(tetrominosToRemove) // Draw title val titleY = height * 0.4f - canvas.drawText("Pixel Mint Drop", width / 2f, titleY, titlePaint) + canvas.drawText("mintris", 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 piece positions - for (piece in pieces) { - piece.x += deltaX * 0.5f - piece.y += deltaY * 0.5f + // Update tetromino positions + for (tetromino in tetrominos) { + tetromino.x += deltaX * 0.5f + tetromino.y += deltaY * 0.5f } lastTouchX = event.x diff --git a/app/src/main/java/com/mintris/model/GameBoard.kt b/app/src/main/java/com/mintris/model/GameBoard.kt new file mode 100644 index 0000000..61bcf11 --- /dev/null +++ b/app/src/main/java/com/mintris/model/GameBoard.kt @@ -0,0 +1,765 @@ +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/pixelmintdrop/model/HighScore.kt b/app/src/main/java/com/mintris/model/HighScore.kt similarity index 79% rename from app/src/main/java/com/pixelmintdrop/model/HighScore.kt rename to app/src/main/java/com/mintris/model/HighScore.kt index ef70516..b4b6295 100644 --- a/app/src/main/java/com/pixelmintdrop/model/HighScore.kt +++ b/app/src/main/java/com/mintris/model/HighScore.kt @@ -1,4 +1,4 @@ -package com.pixelmintdrop.model +package com.mintris.model data class HighScore( val name: String, diff --git a/app/src/main/java/com/pixelmintdrop/model/HighScoreAdapter.kt b/app/src/main/java/com/mintris/model/HighScoreAdapter.kt similarity index 97% rename from app/src/main/java/com/pixelmintdrop/model/HighScoreAdapter.kt rename to app/src/main/java/com/mintris/model/HighScoreAdapter.kt index bcbae60..de4ec52 100644 --- a/app/src/main/java/com/pixelmintdrop/model/HighScoreAdapter.kt +++ b/app/src/main/java/com/mintris/model/HighScoreAdapter.kt @@ -1,4 +1,4 @@ -package com.pixelmintdrop.model +package com.mintris.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.pixelmintdrop.R +import com.mintris.R class HighScoreAdapter : RecyclerView.Adapter() { private var highScores: List = emptyList() diff --git a/app/src/main/java/com/pixelmintdrop/model/HighScoreManager.kt b/app/src/main/java/com/mintris/model/HighScoreManager.kt similarity index 93% rename from app/src/main/java/com/pixelmintdrop/model/HighScoreManager.kt rename to app/src/main/java/com/mintris/model/HighScoreManager.kt index bf35f52..6fd7df9 100644 --- a/app/src/main/java/com/pixelmintdrop/model/HighScoreManager.kt +++ b/app/src/main/java/com/mintris/model/HighScoreManager.kt @@ -1,4 +1,4 @@ -package com.pixelmintdrop.model +package com.mintris.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 = "pixelmintdrop_highscores" + private const val PREFS_NAME = "mintris_highscores" private const val KEY_HIGHSCORES = "highscores" private const val MAX_HIGHSCORES = 5 } diff --git a/app/src/main/java/com/pixelmintdrop/model/PlayerProgressionManager.kt b/app/src/main/java/com/mintris/model/PlayerProgressionManager.kt similarity index 89% rename from app/src/main/java/com/pixelmintdrop/model/PlayerProgressionManager.kt rename to app/src/main/java/com/mintris/model/PlayerProgressionManager.kt index 6a2e066..a395bb7 100644 --- a/app/src/main/java/com/pixelmintdrop/model/PlayerProgressionManager.kt +++ b/app/src/main/java/com/mintris/model/PlayerProgressionManager.kt @@ -1,7 +1,8 @@ -package com.pixelmintdrop.model +package com.mintris.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 @@ -93,31 +94,24 @@ class PlayerProgressionManager(context: Context) { /** * Calculate XP from a game session based on score, lines, level, etc. */ - 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() + 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() } /** @@ -298,7 +292,7 @@ class PlayerProgressionManager(context: Context) { } companion object { - private const val PREFS_NAME = "pixelmintdrop_progression" + private const val PREFS_NAME = "mintris_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" @@ -334,12 +328,6 @@ 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/mintris/model/StatsManager.kt b/app/src/main/java/com/mintris/model/StatsManager.kt new file mode 100644 index 0000000..cc5272f --- /dev/null +++ b/app/src/main/java/com/mintris/model/StatsManager.kt @@ -0,0 +1,196 @@ +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/pixelmintdrop/model/Tetromino.kt b/app/src/main/java/com/mintris/model/Tetromino.kt similarity index 99% rename from app/src/main/java/com/pixelmintdrop/model/Tetromino.kt rename to app/src/main/java/com/mintris/model/Tetromino.kt index d0daf97..54bafb2 100644 --- a/app/src/main/java/com/pixelmintdrop/model/Tetromino.kt +++ b/app/src/main/java/com/mintris/model/Tetromino.kt @@ -1,7 +1,7 @@ -package com.pixelmintdrop.model +package com.mintris.model /** - * Represents a game piece (Tetromino) + * Represents a Tetris piece (Tetromino) */ enum class TetrominoType { I, J, L, O, S, T, Z diff --git a/app/src/main/java/com/pixelmintdrop/ui/BlockSkinSelector.kt b/app/src/main/java/com/mintris/ui/BlockSkinSelector.kt similarity index 99% rename from app/src/main/java/com/pixelmintdrop/ui/BlockSkinSelector.kt rename to app/src/main/java/com/mintris/ui/BlockSkinSelector.kt index 3e1e1c9..d2c7d35 100644 --- a/app/src/main/java/com/pixelmintdrop/ui/BlockSkinSelector.kt +++ b/app/src/main/java/com/mintris/ui/BlockSkinSelector.kt @@ -1,4 +1,4 @@ -package com.pixelmintdrop.ui +package com.mintris.ui import android.content.Context import android.graphics.Color @@ -9,7 +9,9 @@ import android.widget.FrameLayout import android.widget.GridLayout import android.widget.TextView import androidx.cardview.widget.CardView -import com.pixelmintdrop.R +import com.mintris.R +import com.mintris.model.PlayerProgressionManager +import android.animation.ValueAnimator import android.graphics.drawable.GradientDrawable /** diff --git a/app/src/main/java/com/pixelmintdrop/ui/LevelBadge.kt b/app/src/main/java/com/mintris/ui/LevelBadge.kt similarity index 98% rename from app/src/main/java/com/pixelmintdrop/ui/LevelBadge.kt rename to app/src/main/java/com/mintris/ui/LevelBadge.kt index 3213417..67665e4 100644 --- a/app/src/main/java/com/pixelmintdrop/ui/LevelBadge.kt +++ b/app/src/main/java/com/mintris/ui/LevelBadge.kt @@ -1,4 +1,4 @@ -package com.pixelmintdrop.ui +package com.mintris.ui import android.content.Context import android.graphics.Canvas diff --git a/app/src/main/java/com/pixelmintdrop/ui/ProgressionScreen.kt b/app/src/main/java/com/mintris/ui/ProgressionScreen.kt similarity index 99% rename from app/src/main/java/com/pixelmintdrop/ui/ProgressionScreen.kt rename to app/src/main/java/com/mintris/ui/ProgressionScreen.kt index 07d55cc..117a440 100644 --- a/app/src/main/java/com/pixelmintdrop/ui/ProgressionScreen.kt +++ b/app/src/main/java/com/mintris/ui/ProgressionScreen.kt @@ -1,4 +1,4 @@ -package com.pixelmintdrop.ui +package com.mintris.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.pixelmintdrop.R -import com.pixelmintdrop.model.PlayerProgressionManager +import com.mintris.R +import com.mintris.model.PlayerProgressionManager /** * Screen that displays player progression, XP gain, and unlocked rewards diff --git a/app/src/main/java/com/pixelmintdrop/ui/ThemeSelector.kt b/app/src/main/java/com/mintris/ui/ThemeSelector.kt similarity index 99% rename from app/src/main/java/com/pixelmintdrop/ui/ThemeSelector.kt rename to app/src/main/java/com/mintris/ui/ThemeSelector.kt index 059fd6c..ece7cd4 100644 --- a/app/src/main/java/com/pixelmintdrop/ui/ThemeSelector.kt +++ b/app/src/main/java/com/mintris/ui/ThemeSelector.kt @@ -1,4 +1,4 @@ -package com.pixelmintdrop.ui +package com.mintris.ui import android.content.Context import android.graphics.Color @@ -9,8 +9,9 @@ import android.widget.FrameLayout import android.widget.GridLayout import android.widget.TextView import androidx.cardview.widget.CardView -import com.pixelmintdrop.R -import com.pixelmintdrop.model.PlayerProgressionManager +import com.mintris.R +import com.mintris.model.PlayerProgressionManager +import android.animation.ValueAnimator import android.graphics.drawable.GradientDrawable import android.util.Log diff --git a/app/src/main/java/com/pixelmintdrop/ui/XPProgressBar.kt b/app/src/main/java/com/mintris/ui/XPProgressBar.kt similarity index 98% rename from app/src/main/java/com/pixelmintdrop/ui/XPProgressBar.kt rename to app/src/main/java/com/mintris/ui/XPProgressBar.kt index f752e75..930da2f 100644 --- a/app/src/main/java/com/pixelmintdrop/ui/XPProgressBar.kt +++ b/app/src/main/java/com/mintris/ui/XPProgressBar.kt @@ -1,4 +1,4 @@ -package com.pixelmintdrop.ui +package com.mintris.ui import android.animation.ValueAnimator import android.content.Context @@ -9,6 +9,8 @@ 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/java/com/pixelmintdrop/accessibility/GameAccessibilityHelper.kt b/app/src/main/java/com/pixelmintdrop/accessibility/GameAccessibilityHelper.kt deleted file mode 100644 index 45281c0..0000000 --- a/app/src/main/java/com/pixelmintdrop/accessibility/GameAccessibilityHelper.kt +++ /dev/null @@ -1,152 +0,0 @@ -package com.pixelmintdrop.accessibility - -import android.content.Context -import android.os.Build -import android.view.View -import android.view.accessibility.AccessibilityEvent -import android.view.accessibility.AccessibilityManager -import android.widget.ImageButton -import android.widget.TextView -import androidx.core.view.ViewCompat -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 - * or other accessibility needs. - */ -class GameAccessibilityHelper(private val context: Context) { - - private val accessibilityManager = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager - - /** - * Sets up accessibility content descriptions for game controls - */ - fun setupAccessibilityDescriptions( - leftButton: ImageButton?, - rightButton: ImageButton?, - rotateButton: ImageButton?, - dropButton: ImageButton?, - holdButton: ImageButton?, - pauseButton: ImageButton?, - gameView: GameView?, - holdPieceView: View?, - nextPieceView: View? - ) { - // Set content descriptions for all control buttons - leftButton?.contentDescription = context.getString(R.string.accessibility_move_left) - rightButton?.contentDescription = context.getString(R.string.accessibility_move_right) - rotateButton?.contentDescription = context.getString(R.string.accessibility_rotate_piece) - dropButton?.contentDescription = context.getString(R.string.accessibility_drop_piece) - holdButton?.contentDescription = context.getString(R.string.accessibility_hold_piece) - pauseButton?.contentDescription = context.getString(R.string.accessibility_pause_game) - - // Set content descriptions for game views - gameView?.contentDescription = context.getString(R.string.accessibility_game_board) - holdPieceView?.contentDescription = context.getString(R.string.accessibility_held_piece) - nextPieceView?.contentDescription = context.getString(R.string.accessibility_next_piece) - - // Add accessibility delegate to the game view for custom events - gameView?.let { - ViewCompat.setAccessibilityDelegate(it, object : androidx.core.view.AccessibilityDelegateCompat() { - override fun onPopulateAccessibilityEvent(host: View, event: AccessibilityEvent) { - super.onPopulateAccessibilityEvent(host, event) - - // Add custom content to accessibility events if needed - if (event.eventType == AccessibilityEvent.TYPE_VIEW_SELECTED) { - // Example: announce the current piece type - event.text.add("Current piece: ${getCurrentPieceDescription(it)}") - } - } - }) - } - } - - /** - * Announces important game events via accessibility services - */ - fun announceGameEvent(view: View, message: String) { - if (accessibilityManager.isEnabled) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - view.announceForAccessibility(message) - } else { - // For older Android versions - val event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT) - event.text.add(message) - view.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT) - } - } - } - - /** - * Updates score and level information to be more accessible - */ - fun updateGameStatusAccessibility( - scoreTextView: TextView?, - levelTextView: TextView?, - linesTextView: TextView?, - score: Long, - level: Int, - lines: Int - ) { - // Make sure score is accessible to screen readers with proper formatting - scoreTextView?.let { - it.contentDescription = "Score: $score points" - } - - levelTextView?.let { - it.contentDescription = "Level: $level" - } - - linesTextView?.let { - it.contentDescription = "Lines cleared: $lines" - } - } - - /** - * Get a description of the current Tetromino piece for accessibility announcements - */ - private fun getCurrentPieceDescription(gameView: GameView): String { - val pieceType = gameView.getCurrentPieceType() ?: return "No piece" - - return when (pieceType) { - 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" - } - } - - /** - * Announce the game over event with final score - */ - fun announceGameOver(view: View, score: Long) { - announceGameEvent(view, "Game over. Final score: $score points") - } - - /** - * Announce level up - */ - 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/pixelmintdrop/game/GameLifecycleManager.kt b/app/src/main/java/com/pixelmintdrop/game/GameLifecycleManager.kt deleted file mode 100644 index f998875..0000000 --- a/app/src/main/java/com/pixelmintdrop/game/GameLifecycleManager.kt +++ /dev/null @@ -1,154 +0,0 @@ -package com.pixelmintdrop.game - -import android.content.Context -import android.content.SharedPreferences -import android.os.Bundle -import com.google.gson.Gson -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 - * and state saving/restoration. - */ -class GameLifecycleManager(private val context: Context) { - private val sharedPreferences: SharedPreferences = - context.getSharedPreferences("com.com.pixelmintgames.pixelmintdrop.game_state", Context.MODE_PRIVATE) - private val gson = Gson() - - /** - * Save the current game state when the app is paused - */ - fun saveGameState( - gameBoard: GameBoard, - currentScore: Long, - currentLevel: Int, - piecesPlaced: Int, - gameStartTime: Long - ) { - val editor = sharedPreferences.edit() - - // Save primitive data - editor.putLong("current_score", currentScore) - editor.putInt("current_level", currentLevel) - editor.putInt("pieces_placed", piecesPlaced) - editor.putLong("game_start_time", gameStartTime) - - // Save complex GameBoard state - we don't serialize the GameBoard directly - // Instead we save key properties that can be used to recreate the board state - try { - editor.putInt("board_level", gameBoard.level) - editor.putInt("board_lines", gameBoard.lines) - editor.putInt("board_score", gameBoard.score) - } catch (e: Exception) { - // If serialization fails, just clear the saved state - editor.remove("board_level") - editor.remove("board_lines") - editor.remove("board_score") - } - - editor.apply() - } - - /** - * Restore the saved game state when the app is resumed - * Returns a bundle with the restored state or null if no state exists - */ - fun restoreGameState(): Bundle? { - // Check if we have a saved state - if (!sharedPreferences.contains("current_score")) { - return null - } - - val bundle = Bundle() - - // Restore primitive data - bundle.putLong("current_score", sharedPreferences.getLong("current_score", 0)) - bundle.putInt("current_level", sharedPreferences.getInt("current_level", 1)) - bundle.putInt("pieces_placed", sharedPreferences.getInt("pieces_placed", 0)) - bundle.putLong("game_start_time", sharedPreferences.getLong("game_start_time", 0)) - - // We don't try to deserialize the GameBoard, as it's too complex - // Instead, we'll create a new game board and apply saved properties later - bundle.putInt("board_level", sharedPreferences.getInt("board_level", 1)) - bundle.putInt("board_lines", sharedPreferences.getInt("board_lines", 0)) - bundle.putInt("board_score", sharedPreferences.getInt("board_score", 0)) - - return bundle - } - - /** - * Clears the saved game state - */ - fun clearGameState() { - sharedPreferences.edit().clear().apply() - } - - /** - * Handle activity pause - */ - fun onPause( - gameView: GameView?, - gameMusic: GameMusic?, - statsManager: StatsManager?, - highScoreManager: HighScoreManager? - ) { - // Pause the game view - gameView?.let { - if (!it.isPaused && !it.isGameOver()) { - it.pause() - } - } - - // Pause music - gameMusic?.pause() - - // Save any pending stats and scores - these methods must be made public in their respective classes - // or this functionality should be removed if those methods can't be accessed - try { - statsManager?.let { /* Call public save method if available */ } - highScoreManager?.let { /* Call public save method if available */ } - } catch (e: Exception) { - // Log error but continue - } - } - - /** - * Handle activity resume - */ - fun onResume( - gameView: GameView?, - gameMusic: GameMusic?, - isMusicEnabled: Boolean - ) { - // Resume music if enabled - if (isMusicEnabled) { - gameMusic?.resume() - } - } - - /** - * Handle activity destroy - */ - fun onDestroy( - gameView: GameView?, - gameMusic: GameMusic?, - statsManager: StatsManager?, - highScoreManager: HighScoreManager? - ) { - // Release resources - gameView?.releaseResources() - gameMusic?.releaseResources() - - // Save any pending data - these methods must be made public in their respective classes - // or this functionality should be removed if those methods can't be accessed - try { - statsManager?.let { /* Call public save method if available */ } - highScoreManager?.let { /* Call public save method if available */ } - } catch (e: Exception) { - // Log error but continue - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/pixelmintdrop/game/NextPieceView.kt b/app/src/main/java/com/pixelmintdrop/game/NextPieceView.kt deleted file mode 100644 index f6d3543..0000000 --- a/app/src/main/java/com/pixelmintdrop/game/NextPieceView.kt +++ /dev/null @@ -1,97 +0,0 @@ -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/pixelmintdrop/model/GameBoard.kt b/app/src/main/java/com/pixelmintdrop/model/GameBoard.kt deleted file mode 100644 index 35d7d06..0000000 --- a/app/src/main/java/com/pixelmintdrop/model/GameBoard.kt +++ /dev/null @@ -1,199 +0,0 @@ -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 deleted file mode 100644 index 37e0268..0000000 --- a/app/src/main/java/com/pixelmintdrop/model/GamePiece.kt +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index 17ad868..0000000 --- a/app/src/main/java/com/pixelmintdrop/model/GamePieceType.kt +++ /dev/null @@ -1,48 +0,0 @@ -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/pixelmintdrop/model/StatsManager.kt b/app/src/main/java/com/pixelmintdrop/model/StatsManager.kt deleted file mode 100644 index 66dcca5..0000000 --- a/app/src/main/java/com/pixelmintdrop/model/StatsManager.kt +++ /dev/null @@ -1,78 +0,0 @@ -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/pixelmintdrop/theme/ThemeManager.kt b/app/src/main/java/com/pixelmintdrop/theme/ThemeManager.kt deleted file mode 100644 index d87a517..0000000 --- a/app/src/main/java/com/pixelmintdrop/theme/ThemeManager.kt +++ /dev/null @@ -1,52 +0,0 @@ -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/pixelmintdrop/ui/GameUIManager.kt b/app/src/main/java/com/pixelmintdrop/ui/GameUIManager.kt deleted file mode 100644 index 41fa22f..0000000 --- a/app/src/main/java/com/pixelmintdrop/ui/GameUIManager.kt +++ /dev/null @@ -1,187 +0,0 @@ -package com.pixelmintdrop.ui - -import android.content.Context -import android.graphics.Color -import android.view.View -import com.pixelmintdrop.databinding.ActivityMainBinding -import com.pixelmintdrop.model.PlayerProgressionManager - -/** - * Handles UI updates and state management for the game interface - * to reduce the responsibility of the MainActivity. - */ -class GameUIManager( - private val context: Context, - private val binding: ActivityMainBinding -) { - // UI state - private var currentScore = 0L - private var currentLevel = 1 - private var currentLines = 0 - - // Theme management - private var currentTheme = PlayerProgressionManager.THEME_CLASSIC - - /** - * Update the game state UI elements - */ - fun updateGameStateUI(score: Long, level: Int, lines: Int) { - // Update cached values - currentScore = score - currentLevel = level - currentLines = lines - - // Update UI - binding.scoreText.text = score.toString() - binding.currentLevelText.text = level.toString() - binding.linesText.text = lines.toString() - } - - /** - * Show the game over UI - */ - fun showGameOver(finalScore: Long) { - // Hide game UI elements - binding.gameControlsContainer.visibility = View.GONE - binding.holdPieceView.visibility = View.GONE - binding.nextPieceView.visibility = View.GONE - binding.pauseButton.visibility = View.GONE - binding.leftControlsPanel?.visibility = View.GONE - binding.rightControlsPanel?.visibility = View.GONE - - // Show game over container - binding.gameOverContainer.visibility = View.VISIBLE - binding.sessionScoreText.text = "Score: $finalScore" - } - - /** - * Hide the game over UI - */ - fun hideGameOver() { - binding.gameOverContainer.visibility = View.GONE - } - - /** - * Show the pause menu - */ - fun showPauseMenu() { - binding.pauseContainer.visibility = View.VISIBLE - } - - /** - * Hide the pause menu - */ - fun hidePauseMenu() { - binding.pauseContainer.visibility = View.GONE - } - - /** - * Update the music toggle UI based on current state - */ - fun updateMusicToggleUI(isMusicEnabled: Boolean) { - // This assumes there's a musicToggle view in the layout - // Modify as needed based on the actual UI - } - - /** - * Update the level selector display - */ - fun updateLevelSelector(selectedLevel: Int) { - // Assuming pauseLevelText is part of the LevelBadge component - // This may need to be adapted based on how the level badge works - } - - /** - * Show the game elements (views, controls) - */ - fun showGameElements() { - binding.gameView.visibility = View.VISIBLE - binding.gameControlsContainer.visibility = View.VISIBLE - binding.holdPieceView.visibility = View.VISIBLE - binding.nextPieceView.visibility = View.VISIBLE - binding.pauseButton.visibility = View.VISIBLE - - // These might not exist in the actual layout - // binding.leftControlsPanel?.visibility = View.VISIBLE - // binding.rightControlsPanel?.visibility = View.VISIBLE - } - - /** - * Hide the game elements - */ - fun hideGameElements() { - binding.gameControlsContainer.visibility = View.GONE - binding.holdPieceView.visibility = View.GONE - binding.nextPieceView.visibility = View.GONE - binding.pauseButton.visibility = View.GONE - - // These might not exist in the actual layout - // binding.leftControlsPanel?.visibility = View.GONE - // binding.rightControlsPanel?.visibility = View.GONE - } - - /** - * Apply a theme to the game UI - */ - fun applyTheme(themeId: String) { - currentTheme = themeId - - // Set background color based on theme - val backgroundColor = getThemeBackgroundColor(themeId) - binding.root.setBackgroundColor(backgroundColor) - - // Update title screen theme if it has a setTheme method - // binding.titleScreen.setTheme(themeId) - - // Set text colors based on theme - val isDarkTheme = backgroundColor == Color.BLACK || - Color.red(backgroundColor) < 50 && - Color.green(backgroundColor) < 50 && - Color.blue(backgroundColor) < 50 - - val textColor = if (isDarkTheme) Color.WHITE else Color.BLACK - updateTextColors(textColor) - } - - /** - * Update all text colors in the UI - */ - private fun updateTextColors(color: Int) { - binding.scoreText.setTextColor(color) - binding.currentLevelText.setTextColor(color) - binding.linesText.setTextColor(color) - - // Other text views might not exist or have different IDs - // Adapt based on the actual layout - } - - /** - * Get background color based on theme ID - */ - private fun getThemeBackgroundColor(themeId: String): Int { - return when (themeId) { - PlayerProgressionManager.THEME_CLASSIC -> Color.BLACK - PlayerProgressionManager.THEME_NEON -> Color.parseColor("#0D0221") - PlayerProgressionManager.THEME_MONOCHROME -> Color.parseColor("#1A1A1A") - PlayerProgressionManager.THEME_RETRO -> Color.parseColor("#3F2832") - PlayerProgressionManager.THEME_MINIMALIST -> Color.WHITE - PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#0B0C10") - else -> Color.BLACK - } - } - - /** - * Get the current score - */ - fun getCurrentScore(): Long = currentScore - - /** - * Get the current level - */ - fun getCurrentLevel(): Int = currentLevel - - /** - * Get the current lines cleared - */ - fun getCurrentLines(): Int = currentLines -} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 71bd239..5591e04 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/drawable/ic_leaderboard.xml b/app/src/main/res/drawable/ic_leaderboard.xml deleted file mode 100644 index d7cfb64..0000000 --- a/app/src/main/res/drawable/ic_leaderboard.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_play.xml b/app/src/main/res/drawable/ic_play.xml deleted file mode 100644 index 068da4e..0000000 --- a/app/src/main/res/drawable/ic_play.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml index eb8a729..f4a4d50 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"> - - - - Pixel Mint Drop + mintris game over score level @@ -44,26 +44,9 @@ singles: %d doubles: %d triples: %d - quads: %d + tetrises: %d reset stats music Customization Random Mode - - - Play - Start New Game - Scores - View High Scores - - - Rotate piece - Move piece left - Move piece right - Drop piece - Hold current piece - Game board - Next piece preview - Held piece - Pause game \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index a280be0..ea37314 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,7 +1,7 @@ - -