Improve game experience: 1) Adjust swipe controls for better responsiveness, 2) Fix 3D rotation/mirror flip functionality for all pieces, 3) Add line clearing effects

This commit is contained in:
Corey 2025-03-26 01:11:03 -04:00
parent f7f79f2b91
commit 8716102f92
2 changed files with 746 additions and 113 deletions

View file

@ -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) {
}
}
private fun clearRows() {
var linesCleared = 0
// Line clearing animation properties
private var lineClearEffect = false
private var clearedRows = mutableListOf<Int>()
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<Int> = clearedRows
// Get line clear animation progress (0-1)
fun getLineClearProgress(): Float = lineClearProgress
private fun clearRows() {
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]
}
}
clearedRows.add(r)
}
}
// Clear top row
// If we have cleared rows, start the animation
if (clearedRows.isNotEmpty()) {
lineClearEffect = true
lineClearProgress = 0f
lineClearStartTime = System.currentTimeMillis()
// 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[0][c] = EMPTY
board[y][c] = board[y - 1][c]
}
}
linesCleared++
// Clear top row
for (c in 0 until COLS) {
board[0][c] = EMPTY
}
}
if (linesCleared > 0) {
// Update lines and score
lines += linesCleared
// 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)
// 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<Array<Int>>): 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<Array<Int>>, pattern2: Array<Array<Int>>): 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<Array<Int>>, pattern2: Array<Array<Int>>): 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
}
}

View file

@ -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<Star>()
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<View>("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()
}
}
}