Fix board positioning and block alignment issues. Ensure pieces reach bottom and columns have equal width.

This commit is contained in:
cmclark00 2025-03-27 01:54:55 -04:00
parent 5016b6a2f3
commit 7ba18e1a4a
2 changed files with 192 additions and 33 deletions

View file

@ -62,6 +62,7 @@ class GameView @JvmOverloads constructor(
isAntiAlias = true isAntiAlias = true
strokeWidth = 1f strokeWidth = 1f
style = Paint.Style.STROKE style = Paint.Style.STROKE
maskFilter = null // Ensure no blur effect on grid lines
} }
private val glowPaint = Paint().apply { private val glowPaint = Paint().apply {
@ -90,6 +91,15 @@ class GameView @JvmOverloads constructor(
maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER) maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER)
} }
// Add a new paint for the pulse effect
private val pulsePaint = Paint().apply {
color = Color.CYAN
alpha = 255
isAntiAlias = true
style = Paint.Style.FILL
maskFilter = BlurMaskFilter(32f, BlurMaskFilter.Blur.OUTER) // Increased from 16f to 32f
}
// Pre-allocate paint objects to avoid GC // Pre-allocate paint objects to avoid GC
private val tmpPaint = Paint() private val tmpPaint = Paint()
@ -131,6 +141,12 @@ class GameView @JvmOverloads constructor(
var onPieceMove: (() -> Unit)? = null // New callback for piece movement var onPieceMove: (() -> Unit)? = null // New callback for piece movement
var onPieceLock: (() -> Unit)? = null // New callback for piece locking var onPieceLock: (() -> Unit)? = null // New callback for piece locking
// Animation state
private var pulseAnimator: ValueAnimator? = null
private var pulseAlpha = 0f
private var isPulsing = false
private var linesToPulse = mutableListOf<Int>() // Track which lines are being cleared
init { init {
// Start with paused state // Start with paused state
pause() pause()
@ -138,10 +154,15 @@ class GameView @JvmOverloads constructor(
// Connect our callbacks to the GameBoard // Connect our callbacks to the GameBoard
gameBoard.onPieceMove = { onPieceMove?.invoke() } gameBoard.onPieceMove = { onPieceMove?.invoke() }
gameBoard.onPieceLock = { onPieceLock?.invoke() } gameBoard.onPieceLock = { onPieceLock?.invoke() }
gameBoard.onLineClear = { lineCount -> gameBoard.onLineClear = { lineCount, clearedLines ->
android.util.Log.d("GameView", "Received line clear from GameBoard: $lineCount lines") android.util.Log.d("GameView", "Received line clear from GameBoard: $lineCount lines")
try { try {
onLineClear?.invoke(lineCount) onLineClear?.invoke(lineCount)
// Use the lines that were cleared directly
linesToPulse.clear()
linesToPulse.addAll(clearedLines)
android.util.Log.d("GameView", "Found ${linesToPulse.size} lines to pulse")
startPulseAnimation(lineCount)
android.util.Log.d("GameView", "Forwarded line clear callback") android.util.Log.d("GameView", "Forwarded line clear callback")
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("GameView", "Error forwarding line clear callback", e) android.util.Log.e("GameView", "Error forwarding line clear callback", e)
@ -241,15 +262,24 @@ class GameView @JvmOverloads constructor(
val horizontalBlocks = gameBoard.width val horizontalBlocks = gameBoard.width
val verticalBlocks = gameBoard.height val verticalBlocks = gameBoard.height
// Calculate block size to fit within the view // Account for all glow effects and borders
blockSize = min( val borderPadding = 16f // Padding for border glow effects
width.toFloat() / horizontalBlocks,
height.toFloat() / verticalBlocks
)
// Center horizontally and align to bottom // Calculate block size to fit the height exactly, accounting for all padding
boardLeft = (width - (blockSize * horizontalBlocks)) / 2 blockSize = (height.toFloat() - (borderPadding * 2)) / verticalBlocks
boardTop = height - (blockSize * verticalBlocks) // Align to bottom
// Calculate total board width
val totalBoardWidth = blockSize * horizontalBlocks
// Center horizontally
boardLeft = (width - totalBoardWidth) / 2
boardTop = borderPadding // Start with border padding from top
// Calculate the total height needed for the board
val totalHeight = blockSize * verticalBlocks
// Log dimensions for debugging
android.util.Log.d("GameView", "Board dimensions: width=$width, height=$height, blockSize=$blockSize, boardLeft=$boardLeft, boardTop=$boardTop, totalHeight=$totalHeight")
} }
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
@ -297,13 +327,60 @@ class GameView @JvmOverloads constructor(
val bottom = boardTop + gameBoard.height * blockSize val bottom = boardTop + gameBoard.height * blockSize
val rect = RectF(left, top, right, bottom) val rect = RectF(left, top, right, bottom)
// Draw base border with increased glow
borderGlowPaint.apply {
alpha = 80 // Increased from 60
maskFilter = BlurMaskFilter(16f, BlurMaskFilter.Blur.OUTER) // Increased from 8f
}
canvas.drawRect(rect, borderGlowPaint) canvas.drawRect(rect, borderGlowPaint)
// Draw pulsing border if animation is active
if (isPulsing) {
val pulseBorderPaint = Paint().apply {
color = Color.WHITE
style = Paint.Style.STROKE
strokeWidth = 6f + (16f * pulseAlpha) // Increased from 4f+12f to 6f+16f
alpha = (255 * pulseAlpha).toInt()
isAntiAlias = true
maskFilter = BlurMaskFilter(32f * (1f + pulseAlpha), BlurMaskFilter.Blur.OUTER) // Increased from 24f to 32f
}
// Draw the border with a slight inset to prevent edge artifacts
val inset = 1f
canvas.drawRect(
left + inset,
top + inset,
right - inset,
bottom - inset,
pulseBorderPaint
)
// Add an additional outer glow for more dramatic effect
val outerGlowPaint = Paint().apply {
color = Color.WHITE
style = Paint.Style.STROKE
strokeWidth = 2f
alpha = (128 * pulseAlpha).toInt()
isAntiAlias = true
maskFilter = BlurMaskFilter(48f * (1f + pulseAlpha), BlurMaskFilter.Blur.OUTER)
}
canvas.drawRect(
left - 4f,
top - 4f,
right + 4f,
bottom + 4f,
outerGlowPaint
)
}
} }
/** /**
* Draw the grid lines (very subtle) * Draw the grid lines (very subtle)
*/ */
private fun drawGrid(canvas: Canvas) { private fun drawGrid(canvas: Canvas) {
// Save the canvas state to prevent any effects from affecting the grid
canvas.save()
// Draw vertical grid lines // Draw vertical grid lines
for (x in 0..gameBoard.width) { for (x in 0..gameBoard.width) {
val xPos = boardLeft + x * blockSize val xPos = boardLeft + x * blockSize
@ -323,6 +400,9 @@ class GameView @JvmOverloads constructor(
gridPaint gridPaint
) )
} }
// Restore the canvas state
canvas.restore()
} }
/** /**
@ -332,7 +412,7 @@ class GameView @JvmOverloads constructor(
for (y in 0 until gameBoard.height) { for (y in 0 until gameBoard.height) {
for (x in 0 until gameBoard.width) { for (x in 0 until gameBoard.width) {
if (gameBoard.isOccupied(x, y)) { if (gameBoard.isOccupied(x, y)) {
drawBlock(canvas, x, y, false) drawBlock(canvas, x, y, false, y in linesToPulse)
} }
} }
} }
@ -350,10 +430,9 @@ class GameView @JvmOverloads constructor(
val boardX = piece.x + x val boardX = piece.x + x
val boardY = piece.y + y val boardY = piece.y + y
// Only draw if within bounds and visible on screen // Draw piece regardless of vertical position
if (boardY >= 0 && boardY < gameBoard.height && if (boardX >= 0 && boardX < gameBoard.width) {
boardX >= 0 && boardX < gameBoard.width) { drawBlock(canvas, boardX, boardY, false, false)
drawBlock(canvas, boardX, boardY, false)
} }
} }
} }
@ -373,10 +452,9 @@ class GameView @JvmOverloads constructor(
val boardX = piece.x + x val boardX = piece.x + x
val boardY = ghostY + y val boardY = ghostY + y
// Only draw if within bounds and visible on screen // Draw ghost piece regardless of vertical position
if (boardY >= 0 && boardY < gameBoard.height && if (boardX >= 0 && boardX < gameBoard.width) {
boardX >= 0 && boardX < gameBoard.width) { drawBlock(canvas, boardX, boardY, true, false)
drawBlock(canvas, boardX, boardY, true)
} }
} }
} }
@ -386,12 +464,15 @@ class GameView @JvmOverloads constructor(
/** /**
* Draw a single tetris block at the given grid position * Draw a single tetris block at the given grid position
*/ */
private fun drawBlock(canvas: Canvas, x: Int, y: Int, isGhost: Boolean) { private fun drawBlock(canvas: Canvas, x: Int, y: Int, isGhost: Boolean, isPulsingLine: Boolean) {
val left = boardLeft + x * blockSize val left = boardLeft + x * blockSize
val top = boardTop + y * blockSize val top = boardTop + y * blockSize
val right = left + blockSize val right = left + blockSize
val bottom = top + blockSize val bottom = top + blockSize
// Save canvas state before drawing block effects
canvas.save()
// Draw outer glow // Draw outer glow
blockGlowPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE blockGlowPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE
canvas.drawRect(left - 2f, top - 2f, right + 2f, bottom + 2f, blockGlowPaint) canvas.drawRect(left - 2f, top - 2f, right + 2f, bottom + 2f, blockGlowPaint)
@ -406,6 +487,21 @@ class GameView @JvmOverloads constructor(
// Draw inner glow // Draw inner glow
glowPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE glowPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE
canvas.drawRect(left + 1f, top + 1f, right - 1f, bottom - 1f, glowPaint) canvas.drawRect(left + 1f, top + 1f, right - 1f, bottom - 1f, glowPaint)
// Draw pulse effect if animation is active and this is a pulsing line
if (isPulsing && isPulsingLine) {
val pulseBlockPaint = Paint().apply {
color = Color.WHITE
alpha = (255 * pulseAlpha).toInt()
isAntiAlias = true
style = Paint.Style.FILL
maskFilter = BlurMaskFilter(40f * (1f + pulseAlpha), BlurMaskFilter.Blur.OUTER)
}
canvas.drawRect(left - 16f, top - 16f, right + 16f, bottom + 16f, pulseBlockPaint)
}
// Restore canvas state after drawing block effects
canvas.restore()
} }
/** /**
@ -573,10 +669,15 @@ class GameView @JvmOverloads constructor(
// Reconnect callbacks to the new board // Reconnect callbacks to the new board
gameBoard.onPieceMove = { onPieceMove?.invoke() } gameBoard.onPieceMove = { onPieceMove?.invoke() }
gameBoard.onPieceLock = { onPieceLock?.invoke() } gameBoard.onPieceLock = { onPieceLock?.invoke() }
gameBoard.onLineClear = { lineCount -> gameBoard.onLineClear = { lineCount, clearedLines ->
android.util.Log.d("GameView", "Received line clear from GameBoard: $lineCount lines") android.util.Log.d("GameView", "Received line clear from GameBoard: $lineCount lines")
try { try {
onLineClear?.invoke(lineCount) onLineClear?.invoke(lineCount)
// Use the lines that were cleared directly
linesToPulse.clear()
linesToPulse.addAll(clearedLines)
android.util.Log.d("GameView", "Found ${linesToPulse.size} lines to pulse")
startPulseAnimation(lineCount)
android.util.Log.d("GameView", "Forwarded line clear callback") android.util.Log.d("GameView", "Forwarded line clear callback")
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("GameView", "Error forwarding line clear callback", e) android.util.Log.e("GameView", "Error forwarding line clear callback", e)
@ -610,4 +711,42 @@ class GameView @JvmOverloads constructor(
update() update()
invalidate() invalidate()
} }
/**
* Start the pulse animation for line clear
*/
private fun startPulseAnimation(lineCount: Int) {
android.util.Log.d("GameView", "Starting pulse animation for $lineCount lines")
// Cancel any existing animation
pulseAnimator?.cancel()
// Create new animation
pulseAnimator = ValueAnimator.ofFloat(0f, 1f, 0f).apply {
duration = when (lineCount) {
4 -> 2000L // Tetris - longer duration
3 -> 1600L // Triples
2 -> 1200L // Doubles
1 -> 1000L // Singles
else -> 1000L
}
interpolator = LinearInterpolator()
addUpdateListener { animation ->
pulseAlpha = animation.animatedValue as Float
isPulsing = true
invalidate()
android.util.Log.d("GameView", "Pulse animation update: alpha = $pulseAlpha")
}
addListener(object : android.animation.AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: android.animation.Animator) {
isPulsing = false
pulseAlpha = 0f
linesToPulse.clear()
invalidate()
android.util.Log.d("GameView", "Pulse animation ended")
}
})
}
pulseAnimator?.start()
}
} }

View file

@ -49,7 +49,7 @@ class GameBoard(
var onPieceMove: (() -> Unit)? = null var onPieceMove: (() -> Unit)? = null
var onPieceLock: (() -> Unit)? = null var onPieceLock: (() -> Unit)? = null
var onNextPieceChanged: (() -> Unit)? = null var onNextPieceChanged: (() -> Unit)? = null
var onLineClear: ((Int) -> Unit)? = null var onLineClear: ((Int, List<Int>) -> Unit)? = null
init { init {
spawnNextPiece() spawnNextPiece()
@ -226,8 +226,13 @@ class GameBoard(
val boardX = newX + x val boardX = newX + x
val boardY = newY + y val boardY = newY + y
// Check if the position is outside the board // Check if the position is outside the board horizontally
if (boardX < 0 || boardX >= width || boardY >= height) { if (boardX < 0 || boardX >= width) {
return false
}
// Check if the position is below the board
if (boardY >= height) {
return false return false
} }
@ -283,10 +288,12 @@ class GameBoard(
// Quick scan for completed lines // Quick scan for completed lines
var shiftAmount = 0 var shiftAmount = 0
var y = height - 1 var y = height - 1
val linesToClear = mutableListOf<Int>()
while (y >= 0) { while (y >= 0) {
if (grid[y].all { it }) { if (grid[y].all { it }) {
// Line is full, increment shift amount // Line is full, add to lines to clear
linesToClear.add(y)
shiftAmount++ shiftAmount++
} else if (shiftAmount > 0) { } else if (shiftAmount > 0) {
// Shift this row down by shiftAmount // Shift this row down by shiftAmount
@ -295,26 +302,26 @@ class GameBoard(
y-- y--
} }
// Clear top rows
for (y in 0 until shiftAmount) {
java.util.Arrays.fill(grid[y], false)
}
// If lines were cleared, calculate score in background and trigger callback // If lines were cleared, calculate score in background and trigger callback
if (shiftAmount > 0) { if (shiftAmount > 0) {
android.util.Log.d("GameBoard", "Lines cleared: $shiftAmount") android.util.Log.d("GameBoard", "Lines cleared: $shiftAmount")
// Trigger line clear callback on main thread // Trigger line clear callback on main thread with the lines that were cleared
val mainHandler = android.os.Handler(android.os.Looper.getMainLooper()) val mainHandler = android.os.Handler(android.os.Looper.getMainLooper())
mainHandler.post { mainHandler.post {
android.util.Log.d("GameBoard", "Triggering onLineClear callback with $shiftAmount lines") android.util.Log.d("GameBoard", "Triggering onLineClear callback with $shiftAmount lines")
try { try {
onLineClear?.invoke(shiftAmount) onLineClear?.invoke(shiftAmount, linesToClear) // Pass the lines that were cleared
android.util.Log.d("GameBoard", "onLineClear callback completed successfully") android.util.Log.d("GameBoard", "onLineClear callback completed successfully")
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.e("GameBoard", "Error in onLineClear callback", e) android.util.Log.e("GameBoard", "Error in onLineClear callback", e)
} }
} }
// Clear top rows after callback
for (y in 0 until shiftAmount) {
java.util.Arrays.fill(grid[y], false)
}
Thread { Thread {
calculateScore(shiftAmount) calculateScore(shiftAmount)
}.start() }.start()
@ -448,7 +455,8 @@ class GameBoard(
} }
} }
return ghostY // Ensure ghostY doesn't exceed the board height
return ghostY.coerceAtMost(height - 1)
} }
/** /**
@ -467,6 +475,17 @@ class GameBoard(
} }
} }
/**
* Check if a line is completely filled
*/
fun isLineFull(y: Int): Boolean {
return if (y in 0 until height) {
grid[y].all { it }
} else {
false
}
}
/** /**
* Update the current level and adjust game parameters * Update the current level and adjust game parameters
*/ */
@ -499,9 +518,10 @@ class GameBoard(
// Reset game state // Reset game state
score = 0 score = 0
level = 1
lines = 0 lines = 0
isGameOver = false isGameOver = false
dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong() dropInterval = 1000L // Reset to level 1 speed
// Reset scoring state // Reset scoring state
combo = 0 combo = 0