mirror of
https://github.com/cmclark00/tetris-3d.git
synced 2025-05-17 15:15:20 +01:00
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:
parent
f7f79f2b91
commit
8716102f92
2 changed files with 746 additions and 113 deletions
|
@ -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<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() {
|
||||
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<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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue