diff --git a/app/src/main/java/com/pixelmintdrop/game/GameView.kt b/app/src/main/java/com/pixelmintdrop/game/GameView.kt index 305e2f8..4eb2e50 100644 --- a/app/src/main/java/com/pixelmintdrop/game/GameView.kt +++ b/app/src/main/java/com/pixelmintdrop/game/GameView.kt @@ -24,6 +24,7 @@ import com.pixelmintdrop.model.GameBoard import com.pixelmintdrop.model.Tetromino import com.pixelmintdrop.model.TetrominoType import kotlin.math.abs +import kotlin.math.min /** * GameView that renders the Tetris game and handles touch input @@ -58,11 +59,11 @@ class GameView @JvmOverloads constructor( private val borderGlowPaint = Paint().apply { color = Color.WHITE - alpha = 60 + alpha = 90 // Increased from 60 isAntiAlias = true style = Paint.Style.STROKE strokeWidth = 2f - maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER) + maskFilter = BlurMaskFilter(12f, BlurMaskFilter.Blur.OUTER) // Increased from 8f } private val ghostBlockPaint = Paint().apply { @@ -141,7 +142,7 @@ class GameView @JvmOverloads constructor( private var minSwipeVelocity = 1200 // Increased from 800 to require more deliberate swipes private val maxTapMovement = 30f // Increased from 20f to 30f for more lenient tap detection private val minTapTime = 100L // Minimum time for a tap (in milliseconds) - private val rotationCooldown = 150L // Minimum time between rotations (in milliseconds) + private val rotationCooldown = 100L // Reduced from 150L to allow faster rotation taps private val moveCooldown = 50L // Minimum time between move haptics (in milliseconds) private val doubleTapTimeout = 400L // Increased from 300ms to 400ms for more lenient double tap detection private var lastTapX = 0f // X coordinate of last tap @@ -442,28 +443,32 @@ class GameView @JvmOverloads constructor( * 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 - + // Go back to zero padding for flush border + val borderPadding = 0f + + // Calculate available drawing area (entire view) + val availableWidth = width.toFloat() - (borderPadding * 2) + val availableHeight = height.toFloat() - (borderPadding * 2) + + // Calculate potential block sizes based on fitting width and height separately + val blockSizeBasedOnWidth = availableWidth / horizontalBlocks + val blockSizeBasedOnHeight = availableHeight / verticalBlocks + + // Use the smaller block size to ensure the entire board fits within the padded area + blockSize = minOf(blockSizeBasedOnWidth, blockSizeBasedOnHeight) + + // Calculate the final dimensions of the board using the determined block size + val finalBoardWidth = blockSize * horizontalBlocks + val finalBoardHeight = blockSize * verticalBlocks + + // Center the final board area within the entire view + boardLeft = (width.toFloat() - finalBoardWidth) / 2 + boardTop = (height.toFloat() - finalBoardHeight) / 2 + // Log dimensions for debugging - Log.d(TAG, "Board dimensions: width=$width, height=$height, blockSize=$blockSize, boardLeft=$boardLeft, boardTop=$boardTop, totalHeight=$totalHeight") + Log.d(TAG, "Board dimensions (Small Padding, Centered): view($width, $height), block($blockSize), board($finalBoardWidth, $finalBoardHeight), pos($boardLeft, $boardTop)") } override fun onDraw(canvas: Canvas) { @@ -632,12 +637,19 @@ class GameView @JvmOverloads constructor( val rect = RectF(left, top, right, bottom) - // Draw base border with increased glow - borderGlowPaint.apply { - alpha = 80 // Increased from 60 - maskFilter = BlurMaskFilter(16f, BlurMaskFilter.Blur.OUTER) // Increased from 8f - } + // Draw base border with updated glow properties from the paint object canvas.drawRect(rect, borderGlowPaint) + + // Draw a sharp inner line for a flush appearance + val sharpBorderPaint = Paint().apply { + color = Color.WHITE + alpha = 90 // Slightly less intense than glow + isAntiAlias = true + style = Paint.Style.STROKE + strokeWidth = 1f // Very thin line + maskFilter = null // No blur + } + canvas.drawRect(rect, sharpBorderPaint) // Draw pulsing border if animation is active if (isPulsing) { diff --git a/app/src/main/java/com/pixelmintdrop/model/GameBoard.kt b/app/src/main/java/com/pixelmintdrop/model/GameBoard.kt index bca6233..1802863 100644 --- a/app/src/main/java/com/pixelmintdrop/model/GameBoard.kt +++ b/app/src/main/java/com/pixelmintdrop/model/GameBoard.kt @@ -148,17 +148,26 @@ class GameBoard( currentPiece = nextPiece spawnNextPiece() - // Center the piece horizontally and spawn one unit higher + // Center the piece horizontally currentPiece?.apply { x = (width - getWidth()) / 2 - y = -1 // Spawn one unit above the top of the screen - + // Spawn piece at the top row (y=0) + y = 0 + Log.d(TAG, "spawnPiece() - new piece spawned at position (${x},${y}), type=${type}") - // Set the spawn time for the grace period + // Allow holding again if a new piece spawns naturally + if (!isPieceLocking) { // Avoid resetting hold during lockPiece sequence + canHold = true + } + + // Reset soft drop flag + isPlayerSoftDrop = false + + // Record spawn time for grace period pieceSpawnTime = System.currentTimeMillis() - // Check if the piece can be placed (Game Over condition) + // Check if the piece can be placed at y=0 (Game Over condition) if (!canMove(0, 0)) { isGameOver = true Log.d(TAG, "spawnPiece() - Game Over condition detected") @@ -270,42 +279,58 @@ class GameBoard( * Rotate the current piece clockwise */ fun rotate() { - currentPiece?.let { - // Save current rotation - val originalX = it.x - val originalY = it.y - + currentPiece?.let { piece -> + // Save current rotation and position + val originalRotationIndex = piece.getRotationIndex() // Use getter + val originalX = piece.x + val originalY = piece.y + // Try to rotate - it.rotateClockwise() - + piece.rotateClockwise() + Log.d(TAG, "Attempting rotate CW. Original pos: ($originalX, $originalY)") + + // Check if the new rotation is valid at the original position + if (canMove(0, 0)) { + // Rotation is valid without kicks + Log.d(TAG, "Rotate CW successful without kick at: (${piece.x}, ${piece.y})") + onPieceMove?.invoke() + return@let // Exit the let block + } + // 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 + // Define standard kicks (adjust based on SRS rules if needed) + // Order matters: check common kicks first + val kicks = listOf( + Pair(-1, 0), Pair(1, 0), // Basic wall kicks L/R + Pair(-1, -1), Pair(1, -1), // Kicks slighty up (sometimes needed) + Pair(0, -1), // Floor kick + Pair(-2, 0), Pair(2, 0) // Kicks for I piece + // Add more complex SRS kicks if necessary + ) + + var kickApplied = false + for ((kickX, kickY) in kicks) { + // Check canMove relative to the ORIGINAL position + kick offset + // Temporarily set position for the check + piece.x = originalX + kickX + piece.y = originalY + kickY + if (canMove(0, 0)) { // Check validity at the kicked position + Log.d(TAG, "Rotate CW kick applied: ($kickX, $kickY), new pos: (${piece.x}, ${piece.y})") + kickApplied = true + onPieceMove?.invoke() + break // Found a valid kick, stop checking } } + + // Revert if no kick worked + if (!kickApplied) { + Log.d(TAG, "Rotate CW failed - reverting rotation and position") + piece.setRotationIndex(originalRotationIndex) // Use setter + piece.x = originalX // Revert position + piece.y = originalY + // Do not call onPieceMove if rotation failed + } + // No need for onPieceMove here as it's called when kick succeeds or initial rotation is fine } } @@ -313,42 +338,58 @@ class GameBoard( * Rotate the current piece counterclockwise */ fun rotateCounterClockwise() { - currentPiece?.let { - // Save current rotation - val originalX = it.x - val originalY = it.y - + currentPiece?.let { piece -> + // Save current rotation and position + val originalRotationIndex = piece.getRotationIndex() // Use getter + val originalX = piece.x + val originalY = piece.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 - } + piece.rotateCounterClockwise() + Log.d(TAG, "Attempting rotate CCW. Original pos: ($originalX, $originalY)") + + // Check if the new rotation is valid at the original position + if (canMove(0, 0)) { + // Rotation is valid without kicks + Log.d(TAG, "Rotate CCW successful without kick at: (${piece.x}, ${piece.y})") + onPieceMove?.invoke() + return@let // Exit the let block } + + // Wall kick logic - try to move the piece if rotation causes collision + // Define standard kicks (adjust based on SRS rules if needed) + // Order matters: check common kicks first + val kicks = listOf( + Pair(1, 0), Pair(-1, 0), // Basic wall kicks R/L (opposite order might be better for CCW?) + Pair(1, -1), Pair(-1, -1), // Kicks slighty up + Pair(0, -1), // Floor kick + Pair(2, 0), Pair(-2, 0) // Kicks for I piece + // Add more complex SRS kicks if necessary + ) + + var kickApplied = false + for ((kickX, kickY) in kicks) { + // Check canMove relative to the ORIGINAL position + kick offset + // Temporarily set position for the check + piece.x = originalX + kickX + piece.y = originalY + kickY + if (canMove(0, 0)) { // Check validity at the kicked position + Log.d(TAG, "Rotate CCW kick applied: ($kickX, $kickY), new pos: (${piece.x}, ${piece.y})") + kickApplied = true + onPieceMove?.invoke() + break // Found a valid kick, stop checking + } + } + + // Revert if no kick worked + if (!kickApplied) { + Log.d(TAG, "Rotate CCW failed - reverting rotation and position") + piece.setRotationIndex(originalRotationIndex) // Use setter + piece.x = originalX // Revert position + piece.y = originalY + // Do not call onPieceMove if rotation failed + } + // No need for onPieceMove here as it's called when kick succeeds or initial rotation is fine } } @@ -368,7 +409,8 @@ class GameBoard( val boardY = newY + y // Check if the position is outside the board horizontally - if (boardX < 0 || boardX >= width) { + val isOutsideHorizontal = boardX < 0 || boardX >= width + if (isOutsideHorizontal) { return false } @@ -382,8 +424,8 @@ class GameBoard( return false } - // Check if the position is more than one unit above the top of the screen - if (boardY < -1) { + // Check if the position is above the board (top wall collision) + if (boardY < 0) { return false } } diff --git a/app/src/main/java/com/pixelmintdrop/model/Tetromino.kt b/app/src/main/java/com/pixelmintdrop/model/Tetromino.kt index 6f2d708..f0a02d6 100644 --- a/app/src/main/java/com/pixelmintdrop/model/Tetromino.kt +++ b/app/src/main/java/com/pixelmintdrop/model/Tetromino.kt @@ -17,6 +17,21 @@ class Tetromino(val type: TetrominoType) { var x = 0 var y = 0 + /** + * Get the current rotation index (0-3) + */ + fun getRotationIndex(): Int { + return currentRotation + } + + /** + * Set the current rotation index (0-3) + * Internal visibility allows access within the same module (e.g., from GameBoard) + */ + internal fun setRotationIndex(index: Int) { + currentRotation = index.coerceIn(0, 3) // Ensure value stays within 0-3 + } + /** * Get the current shape of the tetromino based on rotation */