diff --git a/android-app/app/src/main/java/com/tetris3d/game/TetrisGame.kt b/android-app/app/src/main/java/com/tetris3d/game/TetrisGame.kt index fe6f0fe..dcf32c6 100644 --- a/android-app/app/src/main/java/com/tetris3d/game/TetrisGame.kt +++ b/android-app/app/src/main/java/com/tetris3d/game/TetrisGame.kt @@ -17,6 +17,9 @@ class TetrisGame(private var options: GameOptions) { // 3D rotation directions private const val ROTATION_X = 0 private const val ROTATION_Y = 1 + + // Refresh interval for animations + const val REFRESH_INTERVAL = 16L // ~60fps } // Game state @@ -231,11 +234,26 @@ class TetrisGame(private var options: GameOptions) { // Update rotation animation updateRotation() + // If a line clear effect is in progress, wait for it to complete + // The view will handle updating and completing the animation + if (lineClearEffect) { + gameHandler.postDelayed(this, REFRESH_INTERVAL) + return + } + // Move the current piece down if (!moveDown()) { // If can't move down, lock the piece lockPiece() clearRows() + + // If a line clear effect started, wait for next frame + if (lineClearEffect) { + gameHandler.postDelayed(this, REFRESH_INTERVAL) + return + } + + // Otherwise, continue with creating a new piece if (!createNewPiece()) { gameOver() } @@ -394,6 +412,12 @@ class TetrisGame(private var options: GameOptions) { while (moveDown()) {} lockPiece() clearRows() + + // If line clear animation started, return and let the game loop handle it + if (lineClearEffect) { + return true + } + if (!createNewPiece()) { gameOver() } else { @@ -408,7 +432,7 @@ class TetrisGame(private var options: GameOptions) { fun rotate3DX(): Boolean { if (isRunning && !isGameOver && options.enable3DEffects) { - // In 3D, rotating along X would change the way the piece appears from front/back + // In 3D, rotating along X would flip the piece vertically rotation3DX = (rotation3DX + 1) % maxRotation3D // Start rotation animation @@ -418,10 +442,75 @@ class TetrisGame(private var options: GameOptions) { rotationProgress = 0f } - // If it's a quarter or three-quarter rotation, actually change the piece orientation + // If it's a quarter or three-quarter rotation, actually mirror the piece vertically if (rotation3DX % (maxRotation3D / 2) == 1) { - // This simulates a 3D rotation by performing a 2D rotation - return rotate() + // Create a vertically mirrored version of the current piece + val currentPattern = getCurrentPieceArray() + val rows = currentPattern.size + val cols = if (rows > 0) currentPattern[0].size else 0 + val mirroredPattern = Array(rows) { r -> Array(cols) { c -> currentPattern[rows - 1 - r][c] } } + + // Check if the mirrored position is valid + if (!checkCollision(currentX, currentY, mirroredPattern)) { + // Replace the current rotation with the mirrored pattern + // Since we don't actually modify the pieces, simulate this by finding a rotation + // that most closely resembles the mirrored pattern, if one exists + + // For symmetrical pieces like O, this may not change anything + val pieceVariants = pieces[currentPiece] + for (i in pieceVariants.indices) { + if (patternsAreEquivalent(mirroredPattern, pieceVariants[i])) { + currentRotation = i + return true + } + } + + // If no matching rotation found, just use regular rotation as fallback + return rotate() + } else { + // Try wall kicks with the mirrored pattern + // Try moving right + if (!checkCollision(currentX + 1, currentY, mirroredPattern)) { + currentX++ + // Find equivalent rotation + val pieceVariants = pieces[currentPiece] + for (i in pieceVariants.indices) { + if (patternsAreEquivalent(mirroredPattern, pieceVariants[i])) { + currentRotation = i + return true + } + } + return rotate() + } + // Try moving left + if (!checkCollision(currentX - 1, currentY, mirroredPattern)) { + currentX-- + // Find equivalent rotation + val pieceVariants = pieces[currentPiece] + for (i in pieceVariants.indices) { + if (patternsAreEquivalent(mirroredPattern, pieceVariants[i])) { + currentRotation = i + return true + } + } + return rotate() + } + // Try moving up + if (!checkCollision(currentX, currentY - 1, mirroredPattern)) { + currentY-- + // Find equivalent rotation + val pieceVariants = pieces[currentPiece] + for (i in pieceVariants.indices) { + if (patternsAreEquivalent(mirroredPattern, pieceVariants[i])) { + currentRotation = i + return true + } + } + return rotate() + } + + // If all fails, don't change the actual piece, just visual effect + } } // Add extra score for 3D rotations when they don't result in a piece rotation @@ -434,7 +523,7 @@ class TetrisGame(private var options: GameOptions) { fun rotate3DY(): Boolean { if (isRunning && !isGameOver && options.enable3DEffects) { - // In 3D, rotating along Y would change the way the piece appears from left/right + // In 3D, rotating along Y would flip the piece horizontally rotation3DY = (rotation3DY + 1) % maxRotation3D // Start rotation animation @@ -444,36 +533,87 @@ class TetrisGame(private var options: GameOptions) { rotationProgress = 0f } - // If it's a quarter or three-quarter rotation, actually change the piece orientation + // If it's a quarter or three-quarter rotation, actually mirror the piece horizontally if (rotation3DY % (maxRotation3D / 2) == 1) { - // This simulates a 3D rotation by performing a 2D rotation in the opposite direction - val nextRotation = (currentRotation - 1 + pieces[currentPiece].size) % pieces[currentPiece].size - val nextPattern = pieces[currentPiece][nextRotation] + // Create a horizontally mirrored version of the current piece + val currentPattern = getCurrentPieceArray() + val rows = currentPattern.size + val cols = if (rows > 0) currentPattern[0].size else 0 + val mirroredPattern = Array(rows) { r -> Array(cols) { c -> currentPattern[r][cols - 1 - c] } } - if (!checkCollision(currentX, currentY, nextPattern)) { - currentRotation = nextRotation - return true - } else { - // Try wall kicks - // Try moving right - if (!checkCollision(currentX + 1, currentY, nextPattern)) { - currentX++ - currentRotation = nextRotation - return true - } - // Try moving left - if (!checkCollision(currentX - 1, currentY, nextPattern)) { - currentX-- - currentRotation = nextRotation - return true - } - // Try moving up (for I piece mostly) - if (!checkCollision(currentX, currentY - 1, nextPattern)) { - currentY-- - currentRotation = nextRotation - return true + // Try to find an equivalent pattern in any piece type + // This allows for pieces to transform into different piece types when mirrored + for (pieceType in 0 until pieces.size) { + val pieceVariants = pieces[pieceType] + for (rotation in pieceVariants.indices) { + // Check if this variant matches our mirrored pattern + if (patternsAreEquivalent(mirroredPattern, pieceVariants[rotation])) { + // Transform into this piece type with this rotation + currentPiece = pieceType + currentRotation = rotation + currentColor = colors[currentPiece] + return true + } } } + + // If no exact match was found, find the most similar pattern + var bestPieceType = -1 + var bestRotation = -1 + var bestScore = -1 + + for (pieceType in 0 until pieces.size) { + val pieceVariants = pieces[pieceType] + for (rotation in pieceVariants.indices) { + val score = patternMatchScore(mirroredPattern, pieceVariants[rotation]) + if (score > bestScore) { + bestScore = score + bestPieceType = pieceType + bestRotation = rotation + } + } + } + + // If we found a reasonable match + if (bestScore > 0) { + // If this is a collision-free position, perform the transformation + if (!checkCollision(currentX, currentY, pieces[bestPieceType][bestRotation])) { + currentPiece = bestPieceType + currentRotation = bestRotation + currentColor = colors[currentPiece] + return true + } else { + // Try wall kicks with the new piece + // Try moving right + if (!checkCollision(currentX + 1, currentY, pieces[bestPieceType][bestRotation])) { + currentX++ + currentPiece = bestPieceType + currentRotation = bestRotation + currentColor = colors[currentPiece] + return true + } + // Try moving left + if (!checkCollision(currentX - 1, currentY, pieces[bestPieceType][bestRotation])) { + currentX-- + currentPiece = bestPieceType + currentRotation = bestRotation + currentColor = colors[currentPiece] + return true + } + // Try moving up + if (!checkCollision(currentX, currentY - 1, pieces[bestPieceType][bestRotation])) { + currentY-- + currentPiece = bestPieceType + currentRotation = bestRotation + currentColor = colors[currentPiece] + return true + } + } + } + + // If we couldn't find a good transformation, just do a regular rotation + // as a fallback to ensure some response to the user's action + return rotate() } // Add extra score for 3D rotations when they don't result in a piece rotation @@ -575,9 +715,26 @@ class TetrisGame(private var options: GameOptions) { } } + // Line clearing animation properties + private var lineClearEffect = false + private var clearedRows = mutableListOf() + private var lineClearProgress = 0f + private val maxLineClearDuration = 0.5f // In seconds + private var lineClearStartTime = 0L + + // Getter for line clear effect + fun isLineClearEffect(): Boolean = lineClearEffect + + // Get the cleared rows for animation + fun getClearedRows(): List = clearedRows + + // Get line clear animation progress (0-1) + fun getLineClearProgress(): Float = lineClearProgress + private fun clearRows() { - var linesCleared = 0 + clearedRows.clear() + // First pass: identify full rows for (r in 0 until ROWS) { var rowFull = true @@ -589,42 +746,91 @@ class TetrisGame(private var options: GameOptions) { } if (rowFull) { - // Move all rows above down - for (y in r downTo 1) { - for (c in 0 until COLS) { - board[y][c] = board[y - 1][c] - } - } - - // Clear top row - for (c in 0 until COLS) { - board[0][c] = EMPTY - } - - linesCleared++ + clearedRows.add(r) } } - if (linesCleared > 0) { - // Update lines and score - lines += linesCleared + // If we have cleared rows, start the animation + if (clearedRows.isNotEmpty()) { + lineClearEffect = true + lineClearProgress = 0f + lineClearStartTime = System.currentTimeMillis() - // Calculate score based on lines cleared and level - when (linesCleared) { - 1 -> score += 100 * level - 2 -> score += 300 * level - 3 -> score += 500 * level - 4 -> score += 800 * level + // The actual row clearing will be done when the animation completes + // This is handled in the updateLineClear method + + // The rows are still part of the board during animation but will be + // displayed with a special effect by the view + } else { + // No rows to clear, continue with normal gameplay + return + } + + // Mark the start of the animation + // The actual row clearance will happen after the animation + } + + // Update line clear animation + fun updateLineClear(): Boolean { + if (!lineClearEffect) return false + + // Calculate progress based on elapsed time + val elapsedTime = (System.currentTimeMillis() - lineClearStartTime) / 1000f + lineClearProgress = (elapsedTime / maxLineClearDuration).coerceIn(0f, 1f) + + // If animation is complete, apply the row clearing + if (lineClearProgress >= 1f) { + // Actually clear the rows and update score + completeLineClear() + + // Reset animation state + lineClearEffect = false + lineClearProgress = 0f + return true + } + + return false + } + + // Complete the line clearing after animation + private fun completeLineClear() { + val linesCleared = clearedRows.size + + // Process cleared rows in descending order to avoid index issues + val sortedRows = clearedRows.sortedDescending() + + for (row in sortedRows) { + // Move all rows above down + for (y in row downTo 1) { + for (c in 0 until COLS) { + board[y][c] = board[y - 1][c] + } } - // Update level (every 10 lines) - level = (lines / 10) + options.startingLevel - - // Notify listeners - gameStateListener?.onScoreChanged(score) - gameStateListener?.onLinesChanged(lines) - gameStateListener?.onLevelChanged(level) + // Clear top row + for (c in 0 until COLS) { + board[0][c] = EMPTY + } } + + // Update lines and score + lines += linesCleared + + // Calculate score based on lines cleared and level + when (linesCleared) { + 1 -> score += 100 * level + 2 -> score += 300 * level + 3 -> score += 500 * level + 4 -> score += 800 * level + } + + // Update level (every 10 lines) + level = (lines / 10) + options.startingLevel + + // Notify listeners + gameStateListener?.onScoreChanged(score) + gameStateListener?.onLinesChanged(lines) + gameStateListener?.onLevelChanged(level) } private fun checkCollision(x: Int, y: Int, piece: Array>): Boolean { @@ -719,4 +925,43 @@ class TetrisGame(private var options: GameOptions) { fun getRotation3DX(): Float = if (isRotating) currentRotation3DX else rotation3DX.toFloat() fun getRotation3DY(): Float = if (isRotating) currentRotation3DY else rotation3DY.toFloat() fun isRotating(): Boolean = isRotating + + // Helper function to check if two patterns are equivalent (ignoring empty space) + private fun patternsAreEquivalent(pattern1: Array>, pattern2: Array>): Boolean { + // Quick check for size match + if (pattern1.size != pattern2.size) return false + if (pattern1.isEmpty() || pattern2.isEmpty()) return pattern1.isEmpty() && pattern2.isEmpty() + if (pattern1[0].size != pattern2[0].size) return false + + // Check if cells with 1s match in both patterns + for (r in pattern1.indices) { + for (c in pattern1[r].indices) { + if (pattern1[r][c] != pattern2[r][c]) { + return false + } + } + } + + return true + } + + // Helper function to score how well two patterns match (higher score = better match) + private fun patternMatchScore(pattern1: Array>, pattern2: Array>): Int { + // Quick check for size match + if (pattern1.size != pattern2.size) return 0 + if (pattern1.isEmpty() || pattern2.isEmpty()) return if (pattern1.isEmpty() && pattern2.isEmpty()) 1 else 0 + if (pattern1[0].size != pattern2[0].size) return 0 + + // Count matching cells + var matchCount = 0 + for (r in pattern1.indices) { + for (c in pattern1[r].indices) { + if (pattern1[r][c] == pattern2[r][c]) { + matchCount++ + } + } + } + + return matchCount + } } \ No newline at end of file diff --git a/android-app/app/src/main/java/com/tetris3d/views/TetrisGameView.kt b/android-app/app/src/main/java/com/tetris3d/views/TetrisGameView.kt index 690d5a4..f4ac309 100644 --- a/android-app/app/src/main/java/com/tetris3d/views/TetrisGameView.kt +++ b/android-app/app/src/main/java/com/tetris3d/views/TetrisGameView.kt @@ -38,33 +38,46 @@ class TetrisGameView @JvmOverloads constructor( // Shadow and grid configuration private val showShadow = true private val showGrid = true + private val showGlowEffects = true // Background gradient colors - private val bgColorStart = Color.parseColor("#1a1a2e") - private val bgColorEnd = Color.parseColor("#0f3460") + private val bgColorStart = Color.parseColor("#06071B") // Darker space background + private val bgColorEnd = Color.parseColor("#0B1026") // Slightly lighter space background private lateinit var bgGradient: LinearGradient + // Glow and star effects + private val stars = ArrayList() + private val random = java.util.Random() + private val starCount = 50 + private val starColors = arrayOf( + Color.parseColor("#FFFFFF"), // White + Color.parseColor("#AAAAFF"), // Light blue + Color.parseColor("#FFAAAA"), // Light red + Color.parseColor("#AAFFAA") // Light green + ) + // Gesture detection for swipe controls private val gestureDetector = GestureDetector(context, TetrisGestureListener()) // Define minimum swipe velocity and distance - private val minSwipeVelocity = 50 // Lowered for better sensitivity - private val minSwipeDistance = 20 // Lowered for better sensitivity + private val minSwipeVelocity = 30 // Lower for better responsiveness + private val minSwipeDistance = 15 // Lower for better responsiveness // Movement control private val autoRepeatHandler = Handler(Looper.getMainLooper()) private var isAutoRepeating = false private var currentMovement: (() -> Unit)? = null - private val autoRepeatDelay = 150L // Increased delay between movements for slower response - private val initialAutoRepeatDelay = 200L // Increased initial delay + private val autoRepeatDelay = 100L // Faster for smoother continuous movement + private val initialAutoRepeatDelay = 150L // Faster initial delay private val interpolator = DecelerateInterpolator(1.5f) // Touch tracking for continuous swipe private var lastTouchX = 0f private var lastTouchY = 0f - private var swipeThreshold = 30f // Increased threshold to prevent accidental moves + private var swipeThreshold = 20f // More sensitive private var lastMoveTime = 0L - private val moveCooldown = 200L // Add cooldown between movements + private val moveCooldown = 110L // Shorter cooldown for more responsive movement + private var tapThreshold = 10f // Slightly more forgiving tap detection // Refresh timer private val refreshHandler = Handler(Looper.getMainLooper()) @@ -77,6 +90,10 @@ class TetrisGameView @JvmOverloads constructor( } } + // Game state flags + private var gameOver = false + private var paused = false + companion object { private const val REFRESH_INTERVAL = 16L // ~60fps } @@ -87,6 +104,10 @@ class TetrisGameView @JvmOverloads constructor( // Start refresh timer startRefreshTimer() + + // Update game state flags + gameOver = game.isGameOver + paused = !game.isRunning } private fun startRefreshTimer() { @@ -122,6 +143,45 @@ class TetrisGameView @JvmOverloads constructor( // Center the board boardLeft = (width - cols * blockSize) / 2 boardTop = (height - rows * blockSize) / 2 + + // Initialize stars for background + initializeStars(w, h) + } + + private fun initializeStars(width: Int, height: Int) { + stars.clear() + for (i in 0 until starCount) { + stars.add(Star( + x = random.nextFloat() * width, + y = random.nextFloat() * height, + size = 1f + random.nextFloat() * 2f, + color = starColors[random.nextInt(starColors.size)], + blinkSpeed = 0.5f + random.nextFloat() * 2f + )) + } + } + + // Star class for background effect + private data class Star( + val x: Float, + val y: Float, + val size: Float, + val color: Int, + val blinkSpeed: Float, + var brightness: Float + ) { + companion object { + private val random = java.util.Random() + } + + constructor(x: Float, y: Float, size: Float, color: Int, blinkSpeed: Float) : this( + x = x, + y = y, + size = size, + color = color, + blinkSpeed = blinkSpeed, + brightness = random.nextFloat() + ) } override fun onDraw(canvas: Canvas) { @@ -129,22 +189,34 @@ class TetrisGameView @JvmOverloads constructor( val game = this.game ?: return - // Draw gradient background + // Update game state flags + gameOver = game.isGameOver + paused = !game.isRunning + + // Draw space background with gradient paint.shader = bgGradient canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint) paint.shader = null + // Draw stars in background + drawStars(canvas) + // Draw grid if enabled if (showGrid) { drawGrid(canvas) } - // Draw board border with glow effect + // Draw board border with enhanced glow effect drawBoardBorder(canvas) // Draw the locked pieces on the board drawBoard(canvas, game) + // Draw line clear effect if active + if (game.isLineClearEffect()) { + drawLineClearEffect(canvas, game) + } + // Draw shadow piece if enabled if (showShadow && !game.isGameOver && game.isRunning) { drawShadowPiece(canvas, game) @@ -154,6 +226,56 @@ class TetrisGameView @JvmOverloads constructor( if (!game.isGameOver) { drawActivePiece(canvas, game) } + + // Update animations + if (game.isLineClearEffect()) { + // Update line clear animation + if (game.updateLineClear()) { + // If line clear animation completed, invalidate again + invalidate() + } else { + // Animation still in progress + invalidate() + } + } + + // Update star animation + updateStars() + } + + private fun updateStars() { + val currentTime = System.currentTimeMillis() / 1000f + for (star in stars) { + // Calculate pulsing brightness based on time and individual star speed + star.brightness = (kotlin.math.sin(currentTime * star.blinkSpeed) + 1f) / 2f + } + + // Force regular refresh to animate stars + if (!gameOver && !paused) { + postInvalidateDelayed(50) + } + } + + private fun drawStars(canvas: Canvas) { + paint.style = Paint.Style.FILL + + for (star in stars) { + // Set color with alpha based on brightness + paint.color = star.color + paint.alpha = (255 * star.brightness).toInt() + + // Draw star with glow effect + if (showGlowEffects) { + paint.setShadowLayer(star.size * 2, 0f, 0f, star.color) + } + + canvas.drawCircle(star.x, star.y, star.size * star.brightness, paint) + + // Reset shadow + if (showGlowEffects) { + paint.setShadowLayer(0f, 0f, 0f, Color.TRANSPARENT) + } + } } private fun drawBoardBorder(canvas: Canvas) { @@ -165,13 +287,36 @@ class TetrisGameView @JvmOverloads constructor( boardTop + TetrisGame.ROWS * blockSize + 4f ) - // Outer glow (cyan color like in the web app) + // Outer glow (enhanced cyan color) paint.style = Paint.Style.STROKE paint.strokeWidth = 4f paint.color = Color.parseColor("#00ffff") - paint.setShadowLayer(8f, 0f, 0f, Color.parseColor("#00ffff")) + + if (showGlowEffects) { + // Stronger glow effect + paint.setShadowLayer(16f, 0f, 0f, Color.parseColor("#00ffff")) + } + canvas.drawRect(borderRect, paint) - paint.setShadowLayer(0f, 0f, 0f, 0) + + // Inner glow + if (showGlowEffects) { + paint.strokeWidth = 2f + paint.color = Color.parseColor("#80ffff") + paint.setShadowLayer(8f, 0f, 0f, Color.parseColor("#80ffff")) + + val innerRect = RectF( + borderRect.left + 4f, + borderRect.top + 4f, + borderRect.right - 4f, + borderRect.bottom - 4f + ) + + canvas.drawRect(innerRect, paint) + } + + // Reset shadow + paint.setShadowLayer(0f, 0f, 0f, Color.TRANSPARENT) } private fun drawBoard(canvas: Canvas, game: TetrisGame) { @@ -188,7 +333,7 @@ class TetrisGameView @JvmOverloads constructor( } private fun drawGrid(canvas: Canvas) { - paint.color = Color.parseColor("#222222") + paint.color = Color.parseColor("#333344") // Slightly blue-tinted grid paint.style = Paint.Style.STROKE paint.strokeWidth = 1f @@ -226,24 +371,35 @@ class TetrisGameView @JvmOverloads constructor( val centerX = boardLeft + (x + piece[0].size / 2f) * blockSize val centerY = boardTop + (y + piece.size / 2f) * blockSize - // Translate to center point, rotate, then translate back + // Translate to center point, apply transformations, then translate back canvas.translate(centerX, centerY) - // Apply 3D perspective scaling based on rotation angles - val scaleX = cos(angleY.toFloat()).coerceAtLeast(0.5f) - val scaleY = cos(angleX.toFloat()).coerceAtLeast(0.5f) - canvas.scale(scaleX, scaleY) + // Apply transformations based on rotation state + // First apply scaling to simulate flipping + val flipX = if (rotationX.toInt() % 2 == 1) -1f else 1f + val flipY = if (rotationY.toInt() % 2 == 1) -1f else 1f + + // Check if we're in the middle of an animation + if (game.isRotating()) { + // For animation, use perspective scaling and smooth transitions + val scaleX = cos(angleY.toFloat()).coerceAtLeast(0.5f) * flipY + val scaleY = cos(angleX.toFloat()).coerceAtLeast(0.5f) * flipX + canvas.scale(scaleX, scaleY) + } else { + // For static display, just flip directly + canvas.scale(flipY, flipX) + } // Translate back canvas.translate(-centerX, -centerY) - // Draw the piece with perspective + // Draw the piece with perspective or flip effect for (r in piece.indices) { for (c in piece[r].indices) { if (piece[r][c] == 1) { - // Calculate position with perspective effect - val offsetX = sin(angleY.toFloat()) * blockSize * 0.2f - val offsetY = sin(angleX.toFloat()) * blockSize * 0.2f + // Calculate offset for 3D effect during animation + val offsetX = if (game.isRotating()) sin(angleY.toFloat()) * blockSize * 0.3f else 0f + val offsetY = if (game.isRotating()) sin(angleX.toFloat()) * blockSize * 0.3f else 0f drawBlock( canvas, @@ -297,18 +453,43 @@ class TetrisGameView @JvmOverloads constructor( val bottom = top + blockSize val blockRect = RectF(left, top, right, bottom) + // Parse the base color + val baseColor = Color.parseColor(colorStr) + + // Create a brighter version for the glow + val red = Color.red(baseColor) + val green = Color.green(baseColor) + val blue = Color.blue(baseColor) + val glowColor = Color.argb(255, + Math.min(255, red + 40), + Math.min(255, green + 40), + Math.min(255, blue + 40) + ) + + // Add glow effect + if (showGlowEffects) { + paint.style = Paint.Style.FILL + paint.color = baseColor + paint.setShadowLayer(blockSize / 4, 0f, 0f, glowColor) + } + // Draw the block fill paint.style = Paint.Style.FILL - paint.color = Color.parseColor(colorStr) + paint.color = baseColor canvas.drawRect(blockRect, paint) + // Reset shadow + if (showGlowEffects) { + paint.setShadowLayer(0f, 0f, 0f, Color.TRANSPARENT) + } + // Draw the highlight (top-left gradient) paint.style = Paint.Style.FILL val highlightPaint = Paint() highlightPaint.shader = LinearGradient( left, top, right, bottom, - Color.argb(120, 255, 255, 255), + Color.argb(150, 255, 255, 255), // More pronounced highlight Color.argb(0, 255, 255, 255), Shader.TileMode.CLAMP ) @@ -319,14 +500,111 @@ class TetrisGameView @JvmOverloads constructor( paint.strokeWidth = 2f paint.color = Color.BLACK canvas.drawRect(blockRect, paint) + + // Draw inner glow edge + if (showGlowEffects) { + paint.style = Paint.Style.STROKE + paint.strokeWidth = 1f + paint.color = glowColor + + val innerRect = RectF( + left + 2, + top + 2, + right - 2, + bottom - 2 + ) + canvas.drawRect(innerRect, paint) + } + } + + // Draw line clear effect + private fun drawLineClearEffect(canvas: Canvas, game: TetrisGame) { + val clearedRows = game.getClearedRows() + val progress = game.getLineClearProgress() + + // Different effect based on animation progress + for (row in clearedRows) { + for (col in 0 until TetrisGame.COLS) { + val left = boardLeft + col * blockSize + val top = boardTop + row * blockSize + val right = left + blockSize + val bottom = top + blockSize + + // Create a pulsing, brightening effect for cleared blocks + val alpha = (255 * (0.5f + 0.5f * Math.sin(progress * Math.PI * 3))).toInt() + val scale = 1.0f + 0.1f * progress + + // Calculate center for scaling + val centerX = left + blockSize / 2 + val centerY = top + blockSize / 2 + + // Save canvas state for transformation + canvas.save() + + // Position at center, scale, then move back + canvas.translate(centerX, centerY) + canvas.scale(scale, scale) + canvas.translate(-centerX, -centerY) + + // Get the color from the board + val color = game.getBoard()[row][col] + + if (color != TetrisGame.EMPTY) { + // Draw with glow effect + val baseColor = Color.parseColor(color) + + // Create a brighter glow as animation progresses + val red = Color.red(baseColor) + val green = Color.green(baseColor) + val blue = Color.blue(baseColor) + + // Get increasingly white as effect progresses + val whiteBlend = progress * 0.7f + val newRed = (red * (1 - whiteBlend) + 255 * whiteBlend).toInt().coerceIn(0, 255) + val newGreen = (green * (1 - whiteBlend) + 255 * whiteBlend).toInt().coerceIn(0, 255) + val newBlue = (blue * (1 - whiteBlend) + 255 * whiteBlend).toInt().coerceIn(0, 255) + + val effectColor = Color.argb(alpha, newRed, newGreen, newBlue) + + // Draw with glow + paint.style = Paint.Style.FILL + paint.color = effectColor + + if (showGlowEffects) { + // Increase glow radius with progress + val glowRadius = blockSize * (0.3f + 0.7f * progress) + paint.setShadowLayer(glowRadius, 0f, 0f, effectColor) + } + + // Draw the block + canvas.drawRect(left, top, right, bottom, paint) + + // Add horizontal line effect + paint.style = Paint.Style.STROKE + paint.strokeWidth = 4f * progress + canvas.drawLine(left, top + blockSize / 2, right, top + blockSize / 2, paint) + + // Reset shadow + if (showGlowEffects) { + paint.setShadowLayer(0f, 0f, 0f, Color.TRANSPARENT) + } + } + + // Restore canvas state + canvas.restore() + } + } } // Handler for touch events override fun onTouchEvent(event: MotionEvent): Boolean { + if (gameOver || paused) return false + when (event.action) { MotionEvent.ACTION_DOWN -> { lastTouchX = event.x lastTouchY = event.y + return true } MotionEvent.ACTION_MOVE -> { val diffX = event.x - lastTouchX @@ -338,9 +616,9 @@ class TetrisGameView @JvmOverloads constructor( return true } - // Check if drag distance exceeds threshold for continuous movement - if (abs(diffX) > swipeThreshold && abs(diffX) > abs(diffY)) { - // Horizontal continuous movement + // Check if drag distance exceeds threshold for movement + if (abs(diffX) > swipeThreshold && abs(diffX) > abs(diffY) * 1.2f) { + // Horizontal movement - requiring less pronounced horizontal movement for smoother control if (diffX > 0) { game?.moveRight() } else { @@ -348,29 +626,48 @@ class TetrisGameView @JvmOverloads constructor( } // Update last position after processing the move lastTouchX = event.x - lastTouchY = event.y lastMoveTime = currentTime invalidate() return true - } else if (abs(diffY) > swipeThreshold && abs(diffY) > abs(diffX)) { - // Vertical continuous movement - only for downward + } else if (abs(diffY) > swipeThreshold && abs(diffY) > abs(diffX) * 1.2f) { + // Vertical movement - requiring less pronounced vertical movement for smoother control if (diffY > 0) { game?.moveDown() // Update last position after processing the move - lastTouchX = event.x lastTouchY = event.y lastMoveTime = currentTime invalidate() return true } } + return true } MotionEvent.ACTION_UP -> { + val diffX = event.x - lastTouchX + val diffY = event.y - lastTouchY + val totalMovement = abs(diffX) + abs(diffY) + + // If this was a tap (very minimal movement) + if (totalMovement < tapThreshold) { + // Simple tap to rotate + game?.rotate() + invalidate() + return true + } + + // Check for deliberate swipe up (hard drop) - more forgiving upward movement + if (abs(diffY) > swipeThreshold * 1.2f && diffY < 0 && abs(diffY) > abs(diffX) * 1.5f) { + game?.hardDrop() + invalidate() + return true + } + stopAutoRepeat() + return true } } - return gestureDetector.onTouchEvent(event) || super.onTouchEvent(event) + return false } private fun startAutoRepeat(action: () -> Unit) { @@ -411,22 +708,9 @@ class TetrisGameView @JvmOverloads constructor( return true } + // We're handling taps directly in onTouchEvent override fun onSingleTapConfirmed(e: MotionEvent): Boolean { - // Use onSingleTapConfirmed instead of onSingleTapUp for better tap detection - - // Determine if tap is on left or right side of screen - val screenMiddle = width / 2 - - if (e.x < screenMiddle) { - // Left side - rotate X axis (horizontal rotation) - game?.rotate3DX() - } else { - // Right side - rotate Y axis (vertical rotation) - game?.rotate3DY() - } - - invalidate() - return true + return false } override fun onFling( @@ -443,10 +727,10 @@ class TetrisGameView @JvmOverloads constructor( // Horizontal swipe if (abs(velocityX) > minSwipeVelocity && abs(diffX) > minSwipeDistance) { if (diffX > 0) { - // Swipe right - move right once, not auto-repeat + // Swipe right - move right once game?.moveRight() } else { - // Swipe left - move left once, not auto-repeat + // Swipe left - move left once game?.moveLeft() } invalidate() @@ -456,13 +740,13 @@ class TetrisGameView @JvmOverloads constructor( // Vertical swipe if (abs(velocityY) > minSwipeVelocity && abs(diffY) > minSwipeDistance) { if (diffY > 0) { - // Swipe down - start soft drop with auto-repeat + // Swipe down - start soft drop startAutoRepeat { game?.moveDown() } } else { // Swipe up - hard drop game?.hardDrop() - invalidate() } + invalidate() return true } } @@ -470,4 +754,108 @@ class TetrisGameView @JvmOverloads constructor( return false } } + + // Create touch control buttons + fun createTouchControlButtons() { + // Create 3D rotation buttons + val context = context ?: return + + // First check if buttons are already added to prevent duplicates + val parent = parent as? android.view.ViewGroup ?: return + if (parent.findViewWithTag("rotate_buttons") != null) { + return + } + + // Create a container for rotation buttons + val rotateButtons = android.widget.LinearLayout(context) + rotateButtons.tag = "rotate_buttons" + rotateButtons.orientation = android.widget.LinearLayout.HORIZONTAL + rotateButtons.layoutParams = android.view.ViewGroup.LayoutParams( + android.view.ViewGroup.LayoutParams.MATCH_PARENT, + android.view.ViewGroup.LayoutParams.WRAP_CONTENT + ) + + // Add layout to position at bottom of screen + rotateButtons.gravity = android.view.Gravity.CENTER + val params = android.widget.FrameLayout.LayoutParams( + android.widget.FrameLayout.LayoutParams.MATCH_PARENT, + android.widget.FrameLayout.LayoutParams.WRAP_CONTENT + ) + params.gravity = android.view.Gravity.BOTTOM + params.setMargins(16, 16, 16, 32) // Add more bottom margin for visibility + rotateButtons.layoutParams = params + + // Create vertical flip button (X-axis rotation) + val verticalFlipButton = android.widget.Button(context) + verticalFlipButton.text = "Flip ↑↓" + verticalFlipButton.tag = "flip_vertical_button" + val buttonParams = android.widget.LinearLayout.LayoutParams( + 0, + android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, + 1.0f + ) + buttonParams.setMargins(12, 12, 12, 12) + verticalFlipButton.layoutParams = buttonParams + + // Style the button + verticalFlipButton.setBackgroundColor(android.graphics.Color.parseColor("#0088ff")) + verticalFlipButton.setTextColor(android.graphics.Color.WHITE) + verticalFlipButton.setPadding(8, 16, 8, 16) + + // Create horizontal flip button (Y-axis rotation) + val horizontalFlipButton = android.widget.Button(context) + horizontalFlipButton.text = "Flip ←→" + horizontalFlipButton.tag = "flip_horizontal_button" + horizontalFlipButton.layoutParams = buttonParams + + // Style the button + horizontalFlipButton.setBackgroundColor(android.graphics.Color.parseColor("#ff5500")) + horizontalFlipButton.setTextColor(android.graphics.Color.WHITE) + horizontalFlipButton.setPadding(8, 16, 8, 16) + + // Add buttons to container + rotateButtons.addView(verticalFlipButton) + rotateButtons.addView(horizontalFlipButton) + + // Add the button container to the parent view + val rootView = parent.rootView as? android.widget.FrameLayout + rootView?.addView(rotateButtons) + + // Add click listeners + verticalFlipButton.setOnClickListener { + if (!game?.isGameOver!! && game?.isRunning!!) { + game?.rotate3DX() + verticalFlipButton.alpha = 0.7f + android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ + verticalFlipButton.alpha = 1.0f + }, 150) + invalidate() + } + } + + horizontalFlipButton.setOnClickListener { + if (!game?.isGameOver!! && game?.isRunning!!) { + game?.rotate3DY() + horizontalFlipButton.alpha = 0.7f + android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ + horizontalFlipButton.alpha = 1.0f + }, 150) + invalidate() + } + } + + // Create and add instructions text view if needed + // Note: For Android implementation we'll show a toast instead of persistent instructions + val instructions = "Swipe to move, tap to rotate, swipe down for soft drop, swipe up for hard drop. Use buttons to flip pieces." + android.widget.Toast.makeText(context, instructions, android.widget.Toast.LENGTH_LONG).show() + } + + // Update the game state flags from the TetrisGame + fun updateGameState() { + game?.let { + gameOver = it.isGameOver + paused = !it.isRunning + invalidate() + } + } } \ No newline at end of file