Enhance: Improve game mechanics and visuals in GameView and GameBoard

- Increased glow effect and adjusted border properties for better visual appeal.
- Updated block size calculation to ensure proper fitting within the view.
- Modified rotation logic in GameBoard to include advanced wall kick mechanics for smoother gameplay.
- Adjusted piece spawning to start from the top row and refined hold mechanics.
- Added getter and setter for rotation index in Tetromino for better encapsulation.
This commit is contained in:
cmclark00 2025-04-01 23:25:40 -04:00
parent 04b87e8f19
commit 9bef4bf003
3 changed files with 170 additions and 101 deletions

View file

@ -24,6 +24,7 @@ import com.pixelmintdrop.model.GameBoard
import com.pixelmintdrop.model.Tetromino import com.pixelmintdrop.model.Tetromino
import com.pixelmintdrop.model.TetrominoType import com.pixelmintdrop.model.TetrominoType
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.min
/** /**
* GameView that renders the Tetris game and handles touch input * GameView that renders the Tetris game and handles touch input
@ -58,11 +59,11 @@ class GameView @JvmOverloads constructor(
private val borderGlowPaint = Paint().apply { private val borderGlowPaint = Paint().apply {
color = Color.WHITE color = Color.WHITE
alpha = 60 alpha = 90 // Increased from 60
isAntiAlias = true isAntiAlias = true
style = Paint.Style.STROKE style = Paint.Style.STROKE
strokeWidth = 2f strokeWidth = 2f
maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER) maskFilter = BlurMaskFilter(12f, BlurMaskFilter.Blur.OUTER) // Increased from 8f
} }
private val ghostBlockPaint = Paint().apply { private val ghostBlockPaint = Paint().apply {
@ -141,7 +142,7 @@ class GameView @JvmOverloads constructor(
private var minSwipeVelocity = 1200 // Increased from 800 to require more deliberate swipes private var minSwipeVelocity = 1200 // Increased from 800 to require more deliberate swipes
private val maxTapMovement = 30f // Increased from 20f to 30f for more lenient tap detection private val maxTapMovement = 30f // Increased from 20f to 30f for more lenient tap detection
private val minTapTime = 100L // Minimum time for a tap (in milliseconds) private val minTapTime = 100L // Minimum time for a tap (in milliseconds)
private val rotationCooldown = 150L // Minimum time between rotations (in milliseconds) private val rotationCooldown = 100L // Reduced from 150L to allow faster rotation taps
private val moveCooldown = 50L // Minimum time between move haptics (in milliseconds) private val moveCooldown = 50L // Minimum time between move haptics (in milliseconds)
private val doubleTapTimeout = 400L // Increased from 300ms to 400ms for more lenient double tap detection private val doubleTapTimeout = 400L // Increased from 300ms to 400ms for more lenient double tap detection
private var lastTapX = 0f // X coordinate of last tap private var lastTapX = 0f // X coordinate of last tap
@ -442,28 +443,32 @@ class GameView @JvmOverloads constructor(
* Calculate dimensions for the board and blocks based on view size * Calculate dimensions for the board and blocks based on view size
*/ */
private fun calculateDimensions(width: Int, height: Int) { private fun calculateDimensions(width: Int, height: Int) {
// Calculate block size based on available space
val horizontalBlocks = gameBoard.width val horizontalBlocks = gameBoard.width
val verticalBlocks = gameBoard.height val verticalBlocks = gameBoard.height
// Go back to zero padding for flush border
// Account for all glow effects and borders val borderPadding = 0f
val borderPadding = 16f // Padding for border glow effects
// Calculate available drawing area (entire view)
// Calculate block size to fit the height exactly, accounting for all padding val availableWidth = width.toFloat() - (borderPadding * 2)
blockSize = (height.toFloat() - (borderPadding * 2)) / verticalBlocks val availableHeight = height.toFloat() - (borderPadding * 2)
// Calculate total board width // Calculate potential block sizes based on fitting width and height separately
val totalBoardWidth = blockSize * horizontalBlocks val blockSizeBasedOnWidth = availableWidth / horizontalBlocks
val blockSizeBasedOnHeight = availableHeight / verticalBlocks
// Center horizontally
boardLeft = (width - totalBoardWidth) / 2 // Use the smaller block size to ensure the entire board fits within the padded area
boardTop = borderPadding // Start with border padding from top blockSize = minOf(blockSizeBasedOnWidth, blockSizeBasedOnHeight)
// Calculate the total height needed for the board // Calculate the final dimensions of the board using the determined block size
val totalHeight = blockSize * verticalBlocks val finalBoardWidth = blockSize * horizontalBlocks
val finalBoardHeight = blockSize * verticalBlocks
// Center the final board area within the entire view
boardLeft = (width.toFloat() - finalBoardWidth) / 2
boardTop = (height.toFloat() - finalBoardHeight) / 2
// Log dimensions for debugging // Log dimensions for debugging
Log.d(TAG, "Board dimensions: width=$width, height=$height, blockSize=$blockSize, boardLeft=$boardLeft, boardTop=$boardTop, totalHeight=$totalHeight") Log.d(TAG, "Board dimensions (Small Padding, Centered): view($width, $height), block($blockSize), board($finalBoardWidth, $finalBoardHeight), pos($boardLeft, $boardTop)")
} }
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
@ -632,12 +637,19 @@ class GameView @JvmOverloads constructor(
val rect = RectF(left, top, right, bottom) val rect = RectF(left, top, right, bottom)
// Draw base border with increased glow // Draw base border with updated glow properties from the paint object
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 a sharp inner line for a flush appearance
val sharpBorderPaint = Paint().apply {
color = Color.WHITE
alpha = 90 // Slightly less intense than glow
isAntiAlias = true
style = Paint.Style.STROKE
strokeWidth = 1f // Very thin line
maskFilter = null // No blur
}
canvas.drawRect(rect, sharpBorderPaint)
// Draw pulsing border if animation is active // Draw pulsing border if animation is active
if (isPulsing) { if (isPulsing) {

View file

@ -148,17 +148,26 @@ class GameBoard(
currentPiece = nextPiece currentPiece = nextPiece
spawnNextPiece() spawnNextPiece()
// Center the piece horizontally and spawn one unit higher // Center the piece horizontally
currentPiece?.apply { currentPiece?.apply {
x = (width - getWidth()) / 2 x = (width - getWidth()) / 2
y = -1 // Spawn one unit above the top of the screen // Spawn piece at the top row (y=0)
y = 0
Log.d(TAG, "spawnPiece() - new piece spawned at position (${x},${y}), type=${type}") Log.d(TAG, "spawnPiece() - new piece spawned at position (${x},${y}), type=${type}")
// Set the spawn time for the grace period // Allow holding again if a new piece spawns naturally
if (!isPieceLocking) { // Avoid resetting hold during lockPiece sequence
canHold = true
}
// Reset soft drop flag
isPlayerSoftDrop = false
// Record spawn time for grace period
pieceSpawnTime = System.currentTimeMillis() pieceSpawnTime = System.currentTimeMillis()
// Check if the piece can be placed (Game Over condition) // Check if the piece can be placed at y=0 (Game Over condition)
if (!canMove(0, 0)) { if (!canMove(0, 0)) {
isGameOver = true isGameOver = true
Log.d(TAG, "spawnPiece() - Game Over condition detected") Log.d(TAG, "spawnPiece() - Game Over condition detected")
@ -270,42 +279,58 @@ class GameBoard(
* Rotate the current piece clockwise * Rotate the current piece clockwise
*/ */
fun rotate() { fun rotate() {
currentPiece?.let { currentPiece?.let { piece ->
// Save current rotation // Save current rotation and position
val originalX = it.x val originalRotationIndex = piece.getRotationIndex() // Use getter
val originalY = it.y val originalX = piece.x
val originalY = piece.y
// Try to rotate // Try to rotate
it.rotateClockwise() piece.rotateClockwise()
Log.d(TAG, "Attempting rotate CW. Original pos: ($originalX, $originalY)")
// Check if the new rotation is valid at the original position
if (canMove(0, 0)) {
// Rotation is valid without kicks
Log.d(TAG, "Rotate CW successful without kick at: (${piece.x}, ${piece.y})")
onPieceMove?.invoke()
return@let // Exit the let block
}
// Wall kick logic - try to move the piece if rotation causes collision // Wall kick logic - try to move the piece if rotation causes collision
if (!canMove(0, 0)) { // Define standard kicks (adjust based on SRS rules if needed)
// Try to move left // Order matters: check common kicks first
if (canMove(-1, 0)) { val kicks = listOf(
it.x-- Pair(-1, 0), Pair(1, 0), // Basic wall kicks L/R
} Pair(-1, -1), Pair(1, -1), // Kicks slighty up (sometimes needed)
// Try to move right Pair(0, -1), // Floor kick
else if (canMove(1, 0)) { Pair(-2, 0), Pair(2, 0) // Kicks for I piece
it.x++ // Add more complex SRS kicks if necessary
} )
// Try to move 2 spaces (for I piece)
else if (canMove(-2, 0)) { var kickApplied = false
it.x -= 2 for ((kickX, kickY) in kicks) {
} // Check canMove relative to the ORIGINAL position + kick offset
else if (canMove(2, 0)) { // Temporarily set position for the check
it.x += 2 piece.x = originalX + kickX
} piece.y = originalY + kickY
// Try to move up for floor kicks if (canMove(0, 0)) { // Check validity at the kicked position
else if (canMove(0, -1)) { Log.d(TAG, "Rotate CW kick applied: ($kickX, $kickY), new pos: (${piece.x}, ${piece.y})")
it.y-- kickApplied = true
} onPieceMove?.invoke()
// Revert if can't find a valid position break // Found a valid kick, stop checking
else {
it.rotateCounterClockwise()
it.x = originalX
it.y = originalY
} }
} }
// Revert if no kick worked
if (!kickApplied) {
Log.d(TAG, "Rotate CW failed - reverting rotation and position")
piece.setRotationIndex(originalRotationIndex) // Use setter
piece.x = originalX // Revert position
piece.y = originalY
// Do not call onPieceMove if rotation failed
}
// No need for onPieceMove here as it's called when kick succeeds or initial rotation is fine
} }
} }
@ -313,42 +338,58 @@ class GameBoard(
* Rotate the current piece counterclockwise * Rotate the current piece counterclockwise
*/ */
fun rotateCounterClockwise() { fun rotateCounterClockwise() {
currentPiece?.let { currentPiece?.let { piece ->
// Save current rotation // Save current rotation and position
val originalX = it.x val originalRotationIndex = piece.getRotationIndex() // Use getter
val originalY = it.y val originalX = piece.x
val originalY = piece.y
// Try to rotate // Try to rotate
it.rotateCounterClockwise() piece.rotateCounterClockwise()
Log.d(TAG, "Attempting rotate CCW. Original pos: ($originalX, $originalY)")
// Wall kick logic - try to move the piece if rotation causes collision
if (!canMove(0, 0)) { // Check if the new rotation is valid at the original position
// Try to move left if (canMove(0, 0)) {
if (canMove(-1, 0)) { // Rotation is valid without kicks
it.x-- Log.d(TAG, "Rotate CCW successful without kick at: (${piece.x}, ${piece.y})")
} onPieceMove?.invoke()
// Try to move right return@let // Exit the let block
else if (canMove(1, 0)) {
it.x++
}
// Try to move 2 spaces (for I piece)
else if (canMove(-2, 0)) {
it.x -= 2
}
else if (canMove(2, 0)) {
it.x += 2
}
// Try to move up for floor kicks
else if (canMove(0, -1)) {
it.y--
}
// Revert if can't find a valid position
else {
it.rotateClockwise()
it.x = originalX
it.y = originalY
}
} }
// Wall kick logic - try to move the piece if rotation causes collision
// Define standard kicks (adjust based on SRS rules if needed)
// Order matters: check common kicks first
val kicks = listOf(
Pair(1, 0), Pair(-1, 0), // Basic wall kicks R/L (opposite order might be better for CCW?)
Pair(1, -1), Pair(-1, -1), // Kicks slighty up
Pair(0, -1), // Floor kick
Pair(2, 0), Pair(-2, 0) // Kicks for I piece
// Add more complex SRS kicks if necessary
)
var kickApplied = false
for ((kickX, kickY) in kicks) {
// Check canMove relative to the ORIGINAL position + kick offset
// Temporarily set position for the check
piece.x = originalX + kickX
piece.y = originalY + kickY
if (canMove(0, 0)) { // Check validity at the kicked position
Log.d(TAG, "Rotate CCW kick applied: ($kickX, $kickY), new pos: (${piece.x}, ${piece.y})")
kickApplied = true
onPieceMove?.invoke()
break // Found a valid kick, stop checking
}
}
// Revert if no kick worked
if (!kickApplied) {
Log.d(TAG, "Rotate CCW failed - reverting rotation and position")
piece.setRotationIndex(originalRotationIndex) // Use setter
piece.x = originalX // Revert position
piece.y = originalY
// Do not call onPieceMove if rotation failed
}
// No need for onPieceMove here as it's called when kick succeeds or initial rotation is fine
} }
} }
@ -368,7 +409,8 @@ class GameBoard(
val boardY = newY + y val boardY = newY + y
// Check if the position is outside the board horizontally // Check if the position is outside the board horizontally
if (boardX < 0 || boardX >= width) { val isOutsideHorizontal = boardX < 0 || boardX >= width
if (isOutsideHorizontal) {
return false return false
} }
@ -382,8 +424,8 @@ class GameBoard(
return false return false
} }
// Check if the position is more than one unit above the top of the screen // Check if the position is above the board (top wall collision)
if (boardY < -1) { if (boardY < 0) {
return false return false
} }
} }

View file

@ -17,6 +17,21 @@ class Tetromino(val type: TetrominoType) {
var x = 0 var x = 0
var y = 0 var y = 0
/**
* Get the current rotation index (0-3)
*/
fun getRotationIndex(): Int {
return currentRotation
}
/**
* Set the current rotation index (0-3)
* Internal visibility allows access within the same module (e.g., from GameBoard)
*/
internal fun setRotationIndex(index: Int) {
currentRotation = index.coerceIn(0, 3) // Ensure value stays within 0-3
}
/** /**
* Get the current shape of the tetromino based on rotation * Get the current shape of the tetromino based on rotation
*/ */