diff --git a/app/src/main/java/com/mintris/MainActivity.kt b/app/src/main/java/com/mintris/MainActivity.kt index f9cb6a8..23a0c5d 100644 --- a/app/src/main/java/com/mintris/MainActivity.kt +++ b/app/src/main/java/com/mintris/MainActivity.kt @@ -16,58 +16,43 @@ import com.mintris.game.GameHaptics import com.mintris.game.GameView import com.mintris.game.NextPieceView import android.view.HapticFeedbackConstants +import com.mintris.model.GameBoard class MainActivity : AppCompatActivity() { // UI components private lateinit var binding: ActivityMainBinding private lateinit var gameView: GameView - private lateinit var scoreText: TextView - private lateinit var levelText: TextView - private lateinit var linesText: TextView - private lateinit var gameOverContainer: LinearLayout - private lateinit var pauseContainer: LinearLayout - private lateinit var playAgainButton: Button - private lateinit var resumeButton: Button - private lateinit var settingsButton: Button - private lateinit var finalScoreText: TextView - private lateinit var nextPieceView: NextPieceView - private lateinit var levelDownButton: Button - private lateinit var levelUpButton: Button - private lateinit var selectedLevelText: TextView private lateinit var gameHaptics: GameHaptics + private lateinit var gameBoard: GameBoard // Game state private var isSoundEnabled = true private var selectedLevel = 1 - private val maxLevel = 10 + private val maxLevel = 20 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) - // Initialize game haptics + // Initialize game components + gameBoard = GameBoard() gameHaptics = GameHaptics(this) + gameView = binding.gameView // Set up game view - gameView = binding.gameView - scoreText = binding.scoreText - levelText = binding.levelText - linesText = binding.linesText - gameOverContainer = binding.gameOverContainer - pauseContainer = binding.pauseContainer - playAgainButton = binding.playAgainButton - resumeButton = binding.resumeButton - settingsButton = binding.settingsButton - finalScoreText = binding.finalScoreText - nextPieceView = binding.nextPieceView - levelDownButton = binding.levelDownButton - levelUpButton = binding.levelUpButton - selectedLevelText = binding.selectedLevelText + gameView.setGameBoard(gameBoard) + gameView.setHaptics(gameHaptics) - // Connect the next piece view to the game view - nextPieceView.setGameView(gameView) + // Set up next piece preview + binding.nextPieceView.setGameView(gameView) + gameBoard.onNextPieceChanged = { + binding.nextPieceView.invalidate() + } + + // Start game immediately + startGame() // Set up callbacks gameView.onGameStateChanged = { score, level, lines -> @@ -99,38 +84,51 @@ class MainActivity : AppCompatActivity() { } // Set up button click listeners with haptic feedback - playAgainButton.setOnClickListener { + binding.playAgainButton.setOnClickListener { gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) hideGameOver() gameView.reset() - setGameLevel(selectedLevel) gameView.start() } - resumeButton.setOnClickListener { + binding.resumeButton.setOnClickListener { gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) hidePauseMenu() gameView.start() } - settingsButton.setOnClickListener { + binding.settingsButton.setOnClickListener { gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) toggleSound() } - - // Set up level selector with haptic feedback - levelDownButton.setOnClickListener { - if (selectedLevel > 1) { - gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) - selectedLevel-- + + // Set up pause menu buttons + binding.pauseStartButton.setOnClickListener { + gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) + hidePauseMenu() + gameView.reset() + gameView.start() + } + + binding.pauseRestartButton.setOnClickListener { + gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) + hidePauseMenu() + gameView.reset() + gameView.start() + } + + binding.pauseLevelUpButton.setOnClickListener { + gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) + if (selectedLevel < maxLevel) { + selectedLevel++ updateLevelSelector() } } - - levelUpButton.setOnClickListener { - if (selectedLevel < maxLevel) { - gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) - selectedLevel++ + + binding.pauseLevelDownButton.setOnClickListener { + gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) + if (selectedLevel > 1) { + selectedLevel-- updateLevelSelector() } } @@ -138,57 +136,30 @@ class MainActivity : AppCompatActivity() { // Initialize level selector updateLevelSelector() - // Start game when clicking the screen initially - setupTouchToStart() - - // Start with the game paused - gameView.pause() - // Enable edge-to-edge display if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.setDecorFitsSystemWindows(false) } } - /** - * Set up touch-to-start behavior for initial screen - */ - private fun setupTouchToStart() { - val touchToStart = View.OnClickListener { - if (gameView.isGameOver()) { - hideGameOver() - gameView.reset() - gameView.start() - } else if (pauseContainer.visibility == View.VISIBLE) { - hidePauseMenu() - gameView.start() - } else { - gameView.start() - } - } - - // Add the click listener to the game view - gameView.setOnClickListener(touchToStart) - } - /** * Update UI with current game state */ private fun updateUI(score: Int, level: Int, lines: Int) { - scoreText.text = score.toString() - levelText.text = level.toString() - linesText.text = lines.toString() + binding.scoreText.text = score.toString() + binding.currentLevelText.text = level.toString() + binding.linesText.text = lines.toString() // Force redraw of next piece preview - nextPieceView.invalidate() + binding.nextPieceView.invalidate() } /** * Show game over screen */ private fun showGameOver(score: Int) { - finalScoreText.text = getString(R.string.score) + ": " + score - gameOverContainer.visibility = View.VISIBLE + binding.finalScoreText.text = getString(R.string.score) + ": " + score + binding.gameOverContainer.visibility = View.VISIBLE // Vibrate to indicate game over vibrate(VibrationEffect.EFFECT_DOUBLE_CLICK) @@ -198,21 +169,23 @@ class MainActivity : AppCompatActivity() { * Hide game over screen */ private fun hideGameOver() { - gameOverContainer.visibility = View.GONE + binding.gameOverContainer.visibility = View.GONE } /** - * Show pause menu + * Show settings menu */ private fun showPauseMenu() { - pauseContainer.visibility = View.VISIBLE + binding.pauseContainer.visibility = View.VISIBLE + binding.pauseStartButton.visibility = View.VISIBLE + binding.resumeButton.visibility = View.GONE } /** - * Hide pause menu + * Hide settings menu */ private fun hidePauseMenu() { - pauseContainer.visibility = View.GONE + binding.pauseContainer.visibility = View.GONE } /** @@ -220,7 +193,7 @@ class MainActivity : AppCompatActivity() { */ private fun toggleSound() { isSoundEnabled = !isSoundEnabled - settingsButton.text = getString( + binding.settingsButton.text = getString( if (isSoundEnabled) R.string.sound_on else R.string.sound_off ) @@ -228,6 +201,14 @@ class MainActivity : AppCompatActivity() { vibrate(VibrationEffect.EFFECT_CLICK) } + /** + * Update the level selector display + */ + private fun updateLevelSelector() { + binding.pauseLevelText.text = selectedLevel.toString() + gameBoard.updateLevel(selectedLevel) + } + /** * Trigger device vibration with predefined effect */ @@ -236,24 +217,18 @@ class MainActivity : AppCompatActivity() { vibrator.vibrate(VibrationEffect.createPredefined(effectId)) } - /** - * Update the level selector display - */ - private fun updateLevelSelector() { - selectedLevelText.text = selectedLevel.toString() + private fun startGame() { + gameView.visibility = View.VISIBLE + gameBoard.startGame() + gameView.start() + hidePauseMenu() } - /** - * Set the game level - */ - private fun setGameLevel(level: Int) { - gameView.gameBoard.level = level - gameView.gameBoard.lines = (level - 1) * 10 - gameView.gameBoard.dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong() - - // Update UI - levelText.text = level.toString() - linesText.text = gameView.gameBoard.lines.toString() + private fun restartGame() { + gameBoard.reset() + gameView.visibility = View.VISIBLE + gameView.start() + showPauseMenu() } override fun onPause() { @@ -261,23 +236,32 @@ class MainActivity : AppCompatActivity() { if (!gameView.isGameOver()) { gameView.pause() showPauseMenu() + binding.pauseStartButton.visibility = View.GONE + binding.resumeButton.visibility = View.VISIBLE } } @Deprecated("Deprecated in Java") override fun onBackPressed() { - if (gameOverContainer.visibility == View.VISIBLE) { + if (binding.gameOverContainer.visibility == View.VISIBLE) { hideGameOver() gameView.reset() return } - if (pauseContainer.visibility == View.GONE) { + if (binding.pauseContainer.visibility == View.GONE) { gameView.pause() showPauseMenu() + binding.pauseStartButton.visibility = View.GONE + binding.resumeButton.visibility = View.VISIBLE } else { hidePauseMenu() gameView.start() } } + + override fun onResume() { + super.onResume() + gameView.resume() + } } \ No newline at end of file diff --git a/app/src/main/java/com/mintris/game/GameView.kt b/app/src/main/java/com/mintris/game/GameView.kt index 5eabf4c..be53364 100644 --- a/app/src/main/java/com/mintris/game/GameView.kt +++ b/app/src/main/java/com/mintris/game/GameView.kt @@ -7,6 +7,7 @@ import android.graphics.Color import android.graphics.Paint import android.graphics.Rect import android.graphics.RectF +import android.graphics.BlurMaskFilter import android.os.Build import android.os.Handler import android.os.Looper @@ -33,12 +34,16 @@ class GameView @JvmOverloads constructor( ) : View(context, attrs, defStyleAttr) { // Game board model - val gameBoard = GameBoard() + private var gameBoard = GameBoard() + private var gameHaptics: GameHaptics? = null // Game state private var isRunning = false private var isPaused = false + // Callbacks + var onNextPieceChanged: (() -> Unit)? = null + // Rendering private val blockPaint = Paint().apply { color = Color.WHITE @@ -53,7 +58,7 @@ class GameView @JvmOverloads constructor( private val gridPaint = Paint().apply { color = Color.parseColor("#222222") // Very dark gray - alpha = 40 + alpha = 20 // Reduced from 40 to be more subtle isAntiAlias = true strokeWidth = 1f style = Paint.Style.STROKE @@ -61,19 +66,28 @@ class GameView @JvmOverloads constructor( private val glowPaint = Paint().apply { color = Color.WHITE - alpha = 80 + alpha = 40 // Reduced from 80 for more subtlety isAntiAlias = true style = Paint.Style.STROKE - strokeWidth = 2f + strokeWidth = 1.5f + maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER) + } + + private val blockGlowPaint = Paint().apply { + color = Color.WHITE + alpha = 60 + isAntiAlias = true + style = Paint.Style.FILL + maskFilter = BlurMaskFilter(12f, BlurMaskFilter.Blur.OUTER) } private val borderGlowPaint = Paint().apply { - color = Color.CYAN - alpha = 120 + color = Color.WHITE + alpha = 60 isAntiAlias = true style = Paint.Style.STROKE - strokeWidth = 4f - setShadowLayer(10f, 0f, 0f, Color.CYAN) + strokeWidth = 2f + maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER) } private val lineClearPaint = Paint().apply { @@ -85,7 +99,7 @@ class GameView @JvmOverloads constructor( // Animation private var lineClearAnimator: ValueAnimator? = null private var lineClearProgress = 0f - private val lineClearDuration = 150L // milliseconds + private val lineClearDuration = 100L // milliseconds // Calculate block size based on view dimensions and board size private var blockSize = 0f @@ -111,10 +125,12 @@ class GameView @JvmOverloads constructor( private var startY = 0f private var lastTapTime = 0L private var lastRotationTime = 0L + private var lastMoveTime = 0L private var minSwipeVelocity = 800 // Minimum velocity for swipe to be considered a hard drop private val maxTapMovement = 20f // Maximum movement allowed for a tap (in pixels) private val minTapTime = 100L // Minimum time for a tap (in milliseconds) private val rotationCooldown = 150L // Minimum time between rotations (in milliseconds) + private val moveCooldown = 50L // Minimum time between move haptics (in milliseconds) // Callback for game events var onGameStateChanged: ((score: Int, level: Int, lines: Int) -> Unit)? = null @@ -156,15 +172,11 @@ class GameView @JvmOverloads constructor( * Start the game */ fun start() { - if (isPaused || !isRunning) { - isPaused = false - if (!isRunning) { - isRunning = true - gameBoard.reset() - } - handler.post(gameLoopRunnable) - invalidate() - } + isPaused = false + isRunning = true + gameBoard.startGame() // Add this line to ensure a new piece is spawned + handler.post(gameLoopRunnable) + invalidate() } /** @@ -184,6 +196,7 @@ class GameView @JvmOverloads constructor( isRunning = false isPaused = true gameBoard.reset() + gameBoard.startGame() // Add this line to ensure a new piece is spawned handler.removeCallbacks(gameLoopRunnable) lineClearAnimator?.cancel() invalidate() @@ -204,14 +217,12 @@ class GameView @JvmOverloads constructor( gameBoard.moveDown() // Check if lines need to be cleared and start animation if needed - if (gameBoard.linesToClear.isNotEmpty() && gameBoard.isLineClearAnimationInProgress) { + if (gameBoard.linesToClear.isNotEmpty()) { // Trigger line clear callback for vibration onLineClear?.invoke(gameBoard.linesToClear.size) - // Start line clearing animation if not already running - if (lineClearAnimator == null || !lineClearAnimator!!.isRunning) { - startLineClearAnimation() - } + // Start line clearing animation immediately + startLineClearAnimation() } // Update UI with current game state @@ -222,8 +233,13 @@ class GameView @JvmOverloads constructor( * Start the line clearing animation */ private fun startLineClearAnimation() { + // Cancel any existing animation lineClearAnimator?.cancel() + // Reset progress + lineClearProgress = 0f + + // Create and start new animation immediately lineClearAnimator = ValueAnimator.ofFloat(0f, 1f).apply { duration = lineClearDuration interpolator = LinearInterpolator() @@ -273,9 +289,9 @@ class GameView @JvmOverloads constructor( height.toFloat() / verticalBlocks ) - // Center the board within the view + // Center horizontally and align to bottom boardLeft = (width - (blockSize * horizontalBlocks)) / 2 - boardTop = (height - (blockSize * verticalBlocks)) / 2 + boardTop = height - (blockSize * verticalBlocks) // Align to bottom } override fun onDraw(canvas: Canvas) { @@ -313,12 +329,11 @@ class GameView @JvmOverloads constructor( private fun drawLineClearAnimation(canvas: Canvas) { // Draw non-clearing blocks for (y in 0 until gameBoard.height) { - // Skip lines that are being cleared if (gameBoard.linesToClear.contains(y)) continue for (x in 0 until gameBoard.width) { if (gameBoard.isOccupied(x, y)) { - drawBlock(canvas, x, y, blockPaint) + drawBlock(canvas, x, y, false) } } } @@ -327,8 +342,8 @@ class GameView @JvmOverloads constructor( for (lineY in gameBoard.linesToClear) { for (x in 0 until gameBoard.width) { // Animation effects for all lines simultaneously - val brightness = 255 - (lineClearProgress * 200).toInt() - val scale = 1.0f - lineClearProgress * 0.5f + val brightness = 255 - (lineClearProgress * 150).toInt() // Reduced from 200 for smoother fade + val scale = 1.0f - lineClearProgress * 0.3f // Reduced from 0.5f for subtler scaling // Set the paint for the clear animation lineClearPaint.color = Color.WHITE @@ -344,8 +359,8 @@ class GameView @JvmOverloads constructor( val rect = RectF(left, top, right, bottom) canvas.drawRect(rect, lineClearPaint) - // Add a glow effect - lineClearPaint.setShadowLayer(10f * (1f - lineClearProgress), 0f, 0f, Color.WHITE) + // Add a more subtle glow effect + lineClearPaint.setShadowLayer(8f * (1f - lineClearProgress), 0f, 0f, Color.WHITE) canvas.drawRect(rect, lineClearPaint) } } @@ -396,7 +411,7 @@ class GameView @JvmOverloads constructor( for (y in 0 until gameBoard.height) { for (x in 0 until gameBoard.width) { if (gameBoard.isOccupied(x, y)) { - drawBlock(canvas, x, y, blockPaint) + drawBlock(canvas, x, y, false) } } } @@ -417,7 +432,7 @@ class GameView @JvmOverloads constructor( // Only draw if within bounds and visible on screen if (boardY >= 0 && boardY < gameBoard.height && boardX >= 0 && boardX < gameBoard.width) { - drawBlock(canvas, boardX, boardY, blockPaint) + drawBlock(canvas, boardX, boardY, false) } } } @@ -440,7 +455,7 @@ class GameView @JvmOverloads constructor( // Only draw if within bounds and visible on screen if (boardY >= 0 && boardY < gameBoard.height && boardX >= 0 && boardX < gameBoard.width) { - drawBlock(canvas, boardX, boardY, ghostBlockPaint) + drawBlock(canvas, boardX, boardY, true) } } } @@ -450,29 +465,26 @@ class GameView @JvmOverloads constructor( /** * Draw a single tetris block at the given grid position */ - private fun drawBlock(canvas: Canvas, x: Int, y: Int, paint: Paint) { + private fun drawBlock(canvas: Canvas, x: Int, y: Int, isGhost: Boolean) { val left = boardLeft + x * blockSize val top = boardTop + y * blockSize val right = left + blockSize val bottom = top + blockSize - // Draw block with a slight inset to create separation - val rect = RectF(left + 1, top + 1, right - 1, bottom - 1) - canvas.drawRect(rect, paint) + // 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 enhanced glow effect - val glowRect = RectF(left, top, right, bottom) - val blockGlowPaint = Paint(glowPaint) - if (paint == blockPaint) { - val piece = gameBoard.getCurrentPiece() - if (piece != null && isPositionInPiece(x, y, piece)) { - // Set glow color based on piece type - blockGlowPaint.color = getTetrominoColor(piece.type) - blockGlowPaint.alpha = 150 - blockGlowPaint.setShadowLayer(3f, 0f, 0f, blockGlowPaint.color) - } + // Draw block + blockPaint.apply { + color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE + alpha = if (isGhost) 30 else 255 } - canvas.drawRect(glowRect, blockGlowPaint) + 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) } /** @@ -524,7 +536,7 @@ class GameView @JvmOverloads constructor( // Check for double tap (rotate) val currentTime = System.currentTimeMillis() - if (currentTime - lastTapTime < 250) { + if (currentTime - lastTapTime < 200) { // Reduced from 250ms for faster response // Double tap detected, rotate the piece if (currentTime - lastRotationTime >= rotationCooldown) { gameBoard.rotate() @@ -538,22 +550,33 @@ class GameView @JvmOverloads constructor( MotionEvent.ACTION_MOVE -> { val deltaX = event.x - lastTouchX val deltaY = event.y - lastTouchY + val currentTime = System.currentTimeMillis() - // Horizontal movement (left/right) - if (abs(deltaX) > blockSize) { + // Horizontal movement (left/right) with reduced threshold + if (abs(deltaX) > blockSize * 0.5f) { // Reduced from 1.0f for more responsive movement if (deltaX > 0) { gameBoard.moveRight() } else { gameBoard.moveLeft() } lastTouchX = event.x + // Add haptic feedback for movement with cooldown + if (currentTime - lastMoveTime >= moveCooldown) { + gameHaptics?.vibrateForPieceMove() + lastMoveTime = currentTime + } invalidate() } - // Vertical movement (soft drop) - if (deltaY > blockSize / 2) { + // Vertical movement (soft drop) with reduced threshold + if (deltaY > blockSize * 0.25f) { // Reduced from 0.5f for more responsive soft drop gameBoard.moveDown() lastTouchY = event.y + // Add haptic feedback for movement with cooldown + if (currentTime - lastMoveTime >= moveCooldown) { + gameHaptics?.vibrateForPieceMove() + lastMoveTime = currentTime + } invalidate() } } @@ -565,7 +588,7 @@ class GameView @JvmOverloads constructor( val deltaX = event.x - startX // If the movement was fast and downward, treat as hard drop - if (moveTime > 0 && deltaY > blockSize && (deltaY / moveTime) * 1000 > minSwipeVelocity) { + if (moveTime > 0 && deltaY > blockSize * 0.5f && (deltaY / moveTime) * 1000 > minSwipeVelocity) { gameBoard.hardDrop() invalidate() } else if (moveTime < minTapTime && @@ -606,9 +629,11 @@ class GameView @JvmOverloads constructor( fun isGameOver(): Boolean = gameBoard.isGameOver /** - * Get the next tetromino + * Get the next piece that will be spawned */ - fun getNextPiece() = gameBoard.getNextPiece() + fun getNextPiece(): Tetromino? { + return gameBoard.getNextPiece() + } /** * Clean up resources when view is detached @@ -617,4 +642,31 @@ class GameView @JvmOverloads constructor( super.onDetachedFromWindow() handler.removeCallbacks(gameLoopRunnable) } + + /** + * Set the game board for this view + */ + fun setGameBoard(board: GameBoard) { + gameBoard = board + invalidate() + } + + /** + * Set the haptics handler for this view + */ + fun setHaptics(haptics: GameHaptics) { + gameHaptics = haptics + } + + /** + * Resume the game + */ + fun resume() { + if (!isRunning) { + isRunning = true + handler.post(gameLoopRunnable) + } + isPaused = false + invalidate() + } } \ No newline at end of file diff --git a/app/src/main/java/com/mintris/game/NextPieceView.kt b/app/src/main/java/com/mintris/game/NextPieceView.kt index 158d058..c82fe2c 100644 --- a/app/src/main/java/com/mintris/game/NextPieceView.kt +++ b/app/src/main/java/com/mintris/game/NextPieceView.kt @@ -5,6 +5,7 @@ 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 @@ -28,10 +29,10 @@ class NextPieceView @JvmOverloads constructor( private val glowPaint = Paint().apply { color = Color.WHITE - alpha = 80 + alpha = 30 isAntiAlias = true style = Paint.Style.STROKE - strokeWidth = 2f + strokeWidth = 1.5f } /** @@ -60,6 +61,20 @@ class NextPieceView @JvmOverloads constructor( 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)) { @@ -68,9 +83,11 @@ class NextPieceView @JvmOverloads constructor( 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) } diff --git a/app/src/main/java/com/mintris/model/GameBoard.kt b/app/src/main/java/com/mintris/model/GameBoard.kt index 8e5a4e3..15c3033 100644 --- a/app/src/main/java/com/mintris/model/GameBoard.kt +++ b/app/src/main/java/com/mintris/model/GameBoard.kt @@ -48,6 +48,7 @@ class GameBoard( // Callbacks for game events var onPieceMove: (() -> Unit)? = null var onPieceLock: (() -> Unit)? = null + var onNextPieceChanged: (() -> Unit)? = null init { spawnNextPiece() @@ -66,6 +67,7 @@ class GameBoard( // Take the next piece from the bag nextPiece = Tetromino(bag.removeFirst()) + onNextPieceChanged?.invoke() } /** @@ -98,6 +100,11 @@ class GameBoard( */ 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 */ @@ -475,11 +482,6 @@ class GameBoard( */ fun getCurrentPiece(): Tetromino? = currentPiece - /** - * Get the next tetromino - */ - fun getNextPiece(): Tetromino? = nextPiece - /** * Check if a cell in the grid is occupied */ @@ -491,6 +493,25 @@ class GameBoard( } } + /** + * Update the current level and adjust game parameters + */ + fun updateLevel(newLevel: Int) { + level = newLevel.coerceIn(1, 20) + // Update game speed based on level (NES formula) + dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong() + } + + /** + * Start a new game + */ + fun startGame() { + reset() + // Initialize pieces + spawnNextPiece() + spawnPiece() + } + /** * Reset the game board */ @@ -504,10 +525,9 @@ class GameBoard( // Reset game state score = 0 - level = 1 lines = 0 isGameOver = false - dropInterval = 1000L + dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong() // Reset scoring state combo = 0 @@ -520,9 +540,9 @@ class GameBoard( canHold = true bag.clear() - // Initialize pieces - spawnNextPiece() - spawnPiece() + // Clear current and next pieces + currentPiece = null + nextPiece = null } /** diff --git a/app/src/main/res/drawable/glow_border.xml b/app/src/main/res/drawable/glow_border.xml new file mode 100644 index 0000000..6bf6c2a --- /dev/null +++ b/app/src/main/res/drawable/glow_border.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 9d8a224..364cb03 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -5,112 +5,79 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/black" + android:fitsSystemWindows="true" tools:context=".MainActivity"> - + + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + + + + + - - + android:textSize="24sp" + android:fontFamily="sans-serif-light" + tools:text="Score: 0" /> - - - - + android:textSize="24sp" + android:fontFamily="sans-serif-light" + tools:text="Level: 1" /> + android:textSize="24sp" + android:fontFamily="sans-serif-light" + tools:text="Lines: 0" /> - - - - - - + - + -