Update game piece handling and rename tetris to quad

This commit is contained in:
cmclark00 2025-04-01 05:31:52 -04:00
parent e26c6ebd8c
commit 7babaeca50
10 changed files with 886 additions and 1738 deletions

View file

@ -10,7 +10,7 @@ import android.widget.TextView
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import com.pixelmintdrop.R import com.pixelmintdrop.R
import com.pixelmintdrop.game.GameView import com.pixelmintdrop.game.GameView
import com.pixelmintdrop.model.TetrominoType import com.pixelmintdrop.model.GamePieceType
/** /**
* Helper class to improve the game's accessibility for users with visual impairments * Helper class to improve the game's accessibility for users with visual impairments
@ -111,13 +111,13 @@ class GameAccessibilityHelper(private val context: Context) {
val pieceType = gameView.getCurrentPieceType() ?: return "No piece" val pieceType = gameView.getCurrentPieceType() ?: return "No piece"
return when (pieceType) { return when (pieceType) {
TetrominoType.I -> "I piece, long bar" GamePieceType.I -> "I piece, long bar"
TetrominoType.J -> "J piece, hook shape pointing left" GamePieceType.J -> "J piece, hook shape pointing left"
TetrominoType.L -> "L piece, hook shape pointing right" GamePieceType.L -> "L piece, hook shape pointing right"
TetrominoType.O -> "O piece, square shape" GamePieceType.O -> "O piece, square shape"
TetrominoType.S -> "S piece, zigzag shape" GamePieceType.S -> "S piece, zigzag shape"
TetrominoType.T -> "T piece, T shape" GamePieceType.T -> "T piece, T shape"
TetrominoType.Z -> "Z piece, reverse zigzag shape" GamePieceType.Z -> "Z piece, reverse zigzag shape"
} }
} }
@ -134,4 +134,19 @@ class GameAccessibilityHelper(private val context: Context) {
fun announceLevelUp(view: View, newLevel: Int) { fun announceLevelUp(view: View, newLevel: Int) {
announceGameEvent(view, "Level up! Now at level $newLevel") announceGameEvent(view, "Level up! Now at level $newLevel")
} }
/**
* Get the piece name for accessibility announcements
*/
private fun getPieceName(type: GamePieceType): String {
return when (type) {
GamePieceType.I -> "I piece"
GamePieceType.J -> "J piece"
GamePieceType.L -> "L piece"
GamePieceType.O -> "O piece"
GamePieceType.S -> "S piece"
GamePieceType.T -> "T piece"
GamePieceType.Z -> "Z piece"
}
}
} }

View file

@ -29,68 +29,6 @@ import com.pixelmintdrop.model.ThemeManager
import com.pixelmintdrop.model.ThemeManager.Theme import com.pixelmintdrop.model.ThemeManager.Theme
import com.pixelmintdrop.model.ThemeManager.ThemeType import com.pixelmintdrop.model.ThemeManager.ThemeType
import com.pixelmintdrop.model.ThemeManager.ThemeVariant import com.pixelmintdrop.model.ThemeManager.ThemeVariant
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.*
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.DARK
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.LIGHT
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_DARK
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_LIGHT
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_DARK
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_LIGHT
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_DARK
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_LIGHT
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_DARK
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_LIGHT
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_DARK
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_LIGHT
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_DARK
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_LIGHT
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_DARK
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_LIGHT
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_DARK
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_LIGHT
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_DARK
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_LIGHT
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_DARK
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_LIGHT
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_DARK
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_LIGHT
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_DARK
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_LIGHT
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_DARK
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_LIGHT
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_DARK
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_LIGHT
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_DARK
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_LIGHT
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_DARK
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_LIGHT
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_DARK
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_LIGHT
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_DARK
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_LIGHT
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM
import com.pixelmintdrop.model.ThemeManager.ThemeVariant.SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_SYSTEM_DARK
import com.pixelmintdrop.model.Tetromino
import com.pixelmintdrop.model.TetrominoType
import kotlin.math.abs import kotlin.math.abs
/** /**
@ -112,9 +50,13 @@ class GameView @JvmOverloads constructor(
// Game state // Game state
private var isRunning = false private var isRunning = false
var isPaused = false // Changed from private to public to allow access from MainActivity var isPaused = false
private var score = 0 private var score = 0
// Current piece
private var currentPiece: GamePiece? = null
private var nextPiece: GamePiece? = null
// Callbacks // Callbacks
var onNextPieceChanged: (() -> Unit)? = null var onNextPieceChanged: (() -> Unit)? = null
@ -135,22 +77,22 @@ class GameView @JvmOverloads constructor(
private val ghostBlockPaint = Paint().apply { private val ghostBlockPaint = Paint().apply {
color = Color.WHITE color = Color.WHITE
alpha = 80 // 30% opacity alpha = 80
isAntiAlias = true isAntiAlias = true
} }
private val gridPaint = Paint().apply { private val gridPaint = Paint().apply {
color = Color.parseColor("#222222") // Very dark gray color = Color.parseColor("#222222")
alpha = 20 // Reduced from 40 to be more subtle alpha = 20
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 maskFilter = null
} }
private val glowPaint = Paint().apply { private val glowPaint = Paint().apply {
color = Color.WHITE color = Color.WHITE
alpha = 40 // Reduced from 80 for more subtlety alpha = 40
isAntiAlias = true isAntiAlias = true
style = Paint.Style.STROKE style = Paint.Style.STROKE
strokeWidth = 1.5f strokeWidth = 1.5f
@ -165,24 +107,20 @@ class GameView @JvmOverloads constructor(
maskFilter = BlurMaskFilter(12f, BlurMaskFilter.Blur.OUTER) maskFilter = BlurMaskFilter(12f, BlurMaskFilter.Blur.OUTER)
} }
// Add a new paint for the pulse effect
private val pulsePaint = Paint().apply { private val pulsePaint = Paint().apply {
color = Color.CYAN color = Color.CYAN
alpha = 255 alpha = 255
isAntiAlias = true isAntiAlias = true
style = Paint.Style.FILL style = Paint.Style.FILL
maskFilter = BlurMaskFilter(32f, BlurMaskFilter.Blur.OUTER) // Increased from 16f to 32f maskFilter = BlurMaskFilter(32f, BlurMaskFilter.Blur.OUTER)
} }
// Pre-allocate paint objects to avoid GC
private val tmpPaint = Paint() private val tmpPaint = Paint()
// Calculate block size based on view dimensions and board size
private var blockSize = 0f private var blockSize = 0f
private var boardLeft = 0f private var boardLeft = 0f
private var boardTop = 0f private var boardTop = 0f
// Game loop handler and runnable
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private val gameLoopRunnable = object : Runnable { private val gameLoopRunnable = object : Runnable {
override fun run() { override fun run() {
@ -194,7 +132,6 @@ class GameView @JvmOverloads constructor(
} }
} }
// Touch parameters
private var lastTouchX = 0f private var lastTouchX = 0f
private var lastTouchY = 0f private var lastTouchY = 0f
private var startX = 0f private var startX = 0f
@ -495,197 +432,29 @@ class GameView @JvmOverloads constructor(
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh) super.onSizeChanged(w, h, oldw, oldh)
// Force hardware acceleration - Critical for performance // Calculate block size based on view dimensions and board size
setLayerType(LAYER_TYPE_HARDWARE, null) blockSize = minOf(
w / (gameBoard.width + 2f), // Add padding
h / (gameBoard.height + 2f) // Add padding
)
// Update gesture exclusion rect for edge-to-edge rendering // Center the board
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { boardLeft = (w - gameBoard.width * blockSize) / 2f
setSystemGestureExclusionRects(listOf(Rect(0, 0, w, h))) boardTop = (h - gameBoard.height * blockSize) / 2f
}
calculateDimensions(w, h)
}
/**
* Calculate dimensions for the board and blocks based on view size
*/
private fun calculateDimensions(width: Int, height: Int) {
// Calculate block size based on available space
val horizontalBlocks = gameBoard.width
val verticalBlocks = gameBoard.height
// Account for all glow effects and borders
val borderPadding = 16f // Padding for border glow effects
// Calculate block size to fit the height exactly, accounting for all padding
blockSize = (height.toFloat() - (borderPadding * 2)) / verticalBlocks
// 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
Log.d(TAG, "Board dimensions: width=$width, height=$height, blockSize=$blockSize, boardLeft=$boardLeft, boardTop=$boardTop, totalHeight=$totalHeight")
} }
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
// Set hardware layer type during draw for better performance
val wasHardwareAccelerated = isHardwareAccelerated
if (!wasHardwareAccelerated) {
setLayerType(LAYER_TYPE_HARDWARE, null)
}
super.onDraw(canvas) super.onDraw(canvas)
// Draw background (already black from theme) // Draw the game board
drawBoard(canvas)
// Draw board border glow // Draw ghost piece
drawBoardBorder(canvas) drawGhostPiece(canvas)
// Draw grid (very subtle) // Draw current piece
drawGrid(canvas) currentPiece?.let { piece ->
drawPiece(canvas, piece)
// Draw locked pieces
drawLockedBlocks(canvas)
if (!gameBoard.isGameOver && isRunning) {
// Draw ghost piece (landing preview)
drawGhostPiece(canvas)
// Draw active piece
drawActivePiece(canvas)
}
// Draw game over effect if animating
if (isGameOverAnimating) {
// First layer - full screen glow
val gameOverPaint = Paint().apply {
color = Color.RED // Change to red for more striking game over indication
alpha = (230 * gameOverAlpha).toInt() // Increased opacity
isAntiAlias = true
style = Paint.Style.FILL
// Only apply blur if alpha is greater than 0
if (gameOverAlpha > 0) {
maskFilter = BlurMaskFilter(64f * gameOverAlpha, BlurMaskFilter.Blur.OUTER)
}
}
// Apply screen shake if active
if (gameOverShakeAmount > 0) {
canvas.save()
val shakeOffsetX = (Math.random() * 2 - 1) * gameOverShakeAmount * 20 // Doubled for more visible shake
val shakeOffsetY = (Math.random() * 2 - 1) * gameOverShakeAmount * 20
canvas.translate(shakeOffsetX.toFloat(), shakeOffsetY.toFloat())
}
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), gameOverPaint)
// Second layer - color transition effect
val gameOverPaint2 = Paint().apply {
// Transition from bright red to theme color
val transitionColor = if (gameOverColorTransition < 0.5f) {
Color.RED
} else {
val transition = (gameOverColorTransition - 0.5f) * 2f
val red = Color.red(currentThemeColor)
val green = Color.green(currentThemeColor)
val blue = Color.blue(currentThemeColor)
Color.argb(
(150 * gameOverAlpha).toInt(), // Increased opacity
(255 - (255-red) * transition).toInt(),
(green * transition).toInt(), // Transition from 0 (red) to theme green
(blue * transition).toInt() // Transition from 0 (red) to theme blue
)
}
color = transitionColor
alpha = (150 * gameOverAlpha).toInt() // Increased opacity
isAntiAlias = true
style = Paint.Style.FILL
// Only apply blur if alpha is greater than 0
if (gameOverAlpha > 0) {
maskFilter = BlurMaskFilter(48f * gameOverAlpha, BlurMaskFilter.Blur.OUTER) // Increased blur
}
}
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), gameOverPaint2)
// Draw "GAME OVER" text
if (gameOverAlpha > 0.5f) {
val textPaint = Paint().apply {
color = Color.WHITE
alpha = (255 * Math.min(1f, (gameOverAlpha - 0.5f) * 2)).toInt()
isAntiAlias = true
textSize = blockSize * 1.5f // Reduced from 2f to 1.5f to fit on screen
textAlign = Paint.Align.CENTER
typeface = android.graphics.Typeface.DEFAULT_BOLD
style = Paint.Style.FILL_AND_STROKE
strokeWidth = blockSize * 0.08f // Proportionally reduced from 0.1f
}
// Draw text with glow
val glowPaint = Paint(textPaint).apply {
maskFilter = BlurMaskFilter(blockSize * 0.4f, BlurMaskFilter.Blur.NORMAL) // Reduced from 0.5f
alpha = (200 * Math.min(1f, (gameOverAlpha - 0.5f) * 2)).toInt()
color = Color.RED
strokeWidth = blockSize * 0.15f // Reduced from 0.2f
}
val xPos = width / 2f
val yPos = height / 3f
// Measure text width to check if it fits
val textWidth = textPaint.measureText("GAME OVER")
// If text would still be too wide, scale it down further
if (textWidth > width * 0.9f) {
val scaleFactor = width * 0.9f / textWidth
textPaint.textSize *= scaleFactor
glowPaint.textSize *= scaleFactor
}
canvas.drawText("GAME OVER", xPos, yPos, glowPaint)
canvas.drawText("GAME OVER", xPos, yPos, textPaint)
}
// Draw falling blocks
if (gameOverBlocksY.isNotEmpty()) {
for (i in gameOverBlocksY.indices) {
val x = gameOverBlocksX[i]
val y = gameOverBlocksY[i]
val rotation = gameOverBlocksRotation[i]
val size = gameOverBlocksSize[i] * blockSize
// Skip blocks that have fallen off the screen
if (y > height) continue
// Draw each falling block with rotation
canvas.save()
canvas.translate(x, y)
canvas.rotate(rotation)
// Create a pulsing effect for the falling blocks
val blockPaint = Paint(blockPaint)
blockPaint.alpha = (255 * gameOverAlpha * (1.0f - y / height.toFloat() * 0.7f)).toInt()
// Draw block with glow effect
val blockGlowPaint = Paint(blockGlowPaint)
blockGlowPaint.alpha = (200 * gameOverAlpha * (1.0f - y / height.toFloat() * 0.5f)).toInt()
canvas.drawRect(-size/2, -size/2, size/2, size/2, blockGlowPaint)
canvas.drawRect(-size/2, -size/2, size/2, size/2, blockPaint)
canvas.restore()
}
}
// Reset any transformations from screen shake
if (gameOverShakeAmount > 0) {
canvas.restore()
}
} }
} }
@ -843,219 +612,52 @@ class GameView @JvmOverloads constructor(
* Draw the ghost piece (landing preview) * Draw the ghost piece (landing preview)
*/ */
private fun drawGhostPiece(canvas: Canvas) { private fun drawGhostPiece(canvas: Canvas) {
val piece = gameBoard.getCurrentPiece() ?: return currentPiece?.let { piece ->
val ghostY = gameBoard.getGhostY() val ghostY = calculateGhostY(piece)
val originalY = piece.y
// Draw semi-transparent background for each block piece.y = ghostY
for (y in 0 until piece.getHeight()) {
for (x in 0 until piece.getWidth()) { ghostBlockPaint.alpha = 50
if (piece.isBlockAt(x, y)) { drawPiece(canvas, piece, alpha = 50)
val boardX = piece.x + x
val boardY = ghostY + y piece.y = originalY
if (boardX >= 0 && boardX < gameBoard.width) {
val screenX = boardLeft + boardX * blockSize
val screenY = boardTop + boardY * blockSize
// Draw background
canvas.drawRect(
screenX + 1f,
screenY + 1f,
screenX + blockSize - 1f,
screenY + blockSize - 1f,
ghostBackgroundPaint
)
// Draw border
canvas.drawRect(
screenX + 1f,
screenY + 1f,
screenX + blockSize - 1f,
screenY + blockSize - 1f,
ghostBorderPaint
)
// Draw outline
canvas.drawRect(
screenX + 1f,
screenY + 1f,
screenX + blockSize - 1f,
screenY + blockSize - 1f,
ghostPaint
)
}
}
}
} }
} }
/** private fun calculateGhostY(piece: GamePiece): Int {
* Draw a single tetris block at the given grid position var testY = piece.y
*/ while (testY < gameBoard.height && !gameBoard.wouldCollide(piece, piece.x, testY + 1)) {
private fun drawBlock(canvas: Canvas, x: Int, y: Int, isGhost: Boolean, isPulsingLine: Boolean) { testY++
val left = boardLeft + x * blockSize }
val top = boardTop + y * blockSize return testY
val right = left + blockSize }
val bottom = top + blockSize
private fun drawPiece(canvas: Canvas, piece: GamePiece, offsetX: Float = 0f, offsetY: Float = 0f, alpha: Int = 255) {
val width = piece.getWidth()
val height = piece.getHeight()
// Save canvas state before drawing block effects blockPaint.alpha = alpha
canvas.save() for (y in 0 until height) {
for (x in 0 until width) {
// Get the current block skin paint if (piece.isBlockAt(x, y)) {
val paint = blockSkinPaints[currentBlockSkin] ?: blockSkinPaints["block_skin_1"]!! val left = offsetX + (piece.x + x) * blockSize
val top = offsetY + (piece.y + y) * blockSize
// Create a clone of the paint to avoid modifying the original val right = left + blockSize
val blockPaint = Paint(paint) val bottom = top + blockSize
// Draw block based on current skin // Draw block with subtle glow
when (currentBlockSkin) { blockGlowPaint.alpha = alpha / 4
"block_skin_1" -> { // Classic canvas.drawRect(left, top, right, bottom, blockGlowPaint)
// Draw outer glow
blockGlowPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE // Draw the main block
canvas.drawRect(left - 2f, top - 2f, right + 2f, bottom + 2f, blockGlowPaint) canvas.drawRect(left + 1f, top + 1f, right - 1f, bottom - 1f, blockPaint)
// Draw block // Draw border glow
blockPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE glowPaint.alpha = alpha / 2
blockPaint.alpha = if (isGhost) 30 else 255 canvas.drawRect(left, top, right, bottom, glowPaint)
canvas.drawRect(left, top, right, bottom, blockPaint)
// Draw inner glow
glowPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE
canvas.drawRect(left + 1f, top + 1f, right - 1f, bottom - 1f, glowPaint)
}
"block_skin_2" -> { // Neon
// Stronger outer glow for neon skin
blockGlowPaint.color = if (isGhost) Color.argb(30, 255, 0, 255) else Color.parseColor("#FF00FF")
blockGlowPaint.maskFilter = BlurMaskFilter(16f, BlurMaskFilter.Blur.OUTER)
canvas.drawRect(left - 4f, top - 4f, right + 4f, bottom + 4f, blockGlowPaint)
// For neon, use semi-translucent fill with strong glowing edges
blockPaint.style = Paint.Style.FILL_AND_STROKE
blockPaint.strokeWidth = 2f
blockPaint.maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.NORMAL)
if (isGhost) {
blockPaint.color = Color.argb(30, 255, 0, 255)
blockPaint.alpha = 30
} else {
blockPaint.color = Color.parseColor("#66004D") // Darker magenta fill
blockPaint.alpha = 170 // More opaque to be more visible
}
// Draw block with neon effect
canvas.drawRect(left, top, right, bottom, blockPaint)
// Draw a brighter border for better visibility
val borderPaint = Paint().apply {
color = Color.parseColor("#FF00FF")
style = Paint.Style.STROKE
strokeWidth = 3f
alpha = 255
isAntiAlias = true
maskFilter = BlurMaskFilter(6f, BlurMaskFilter.Blur.NORMAL)
}
canvas.drawRect(left, top, right, bottom, borderPaint)
// Inner glow for neon blocks
glowPaint.color = if (isGhost) Color.argb(10, 255, 0, 255) else Color.parseColor("#FF00FF")
glowPaint.alpha = if (isGhost) 10 else 100
glowPaint.style = Paint.Style.STROKE
glowPaint.strokeWidth = 2f
glowPaint.maskFilter = BlurMaskFilter(4f, BlurMaskFilter.Blur.NORMAL)
canvas.drawRect(left + 4f, top + 4f, right - 4f, bottom - 4f, glowPaint)
}
"block_skin_3" -> { // Retro
// Draw pixelated block with retro effect
blockPaint.color = if (isGhost) Color.argb(30, 255, 90, 95) else Color.parseColor("#FF5A5F")
blockPaint.alpha = if (isGhost) 30 else 255
// Draw main block
canvas.drawRect(left, top, right, bottom, blockPaint)
// Draw pixelated highlights
val highlightPaint = Paint().apply {
color = Color.parseColor("#FF8A8F")
isAntiAlias = false
style = Paint.Style.FILL
}
// Top and left highlights
canvas.drawRect(left, top, right - 2f, top + 2f, highlightPaint)
canvas.drawRect(left, top, left + 2f, bottom - 2f, highlightPaint)
// Draw pixelated shadows
val shadowPaint = Paint().apply {
color = Color.parseColor("#CC4A4F")
isAntiAlias = false
style = Paint.Style.FILL
}
// Bottom and right shadows
canvas.drawRect(left + 2f, bottom - 2f, right, bottom, shadowPaint)
canvas.drawRect(right - 2f, top + 2f, right, bottom - 2f, shadowPaint)
}
"block_skin_4" -> { // Minimalist
// Draw clean, simple block with subtle border
blockPaint.color = if (isGhost) Color.argb(30, 0, 0, 0) else Color.BLACK
blockPaint.alpha = if (isGhost) 30 else 255
blockPaint.style = Paint.Style.FILL
canvas.drawRect(left, top, right, bottom, blockPaint)
// Draw subtle border
val borderPaint = Paint().apply {
color = Color.parseColor("#333333")
style = Paint.Style.STROKE
strokeWidth = 1f
isAntiAlias = true
}
canvas.drawRect(left, top, right, bottom, borderPaint)
}
"block_skin_5" -> { // Galaxy
// Draw cosmic glow effect
blockGlowPaint.color = if (isGhost) Color.argb(30, 102, 252, 241) else Color.parseColor("#66FCF1")
blockGlowPaint.maskFilter = BlurMaskFilter(20f, BlurMaskFilter.Blur.OUTER)
canvas.drawRect(left - 8f, top - 8f, right + 8f, bottom + 8f, blockGlowPaint)
// Draw main block with gradient
val gradient = LinearGradient(
left, top, right, bottom,
Color.parseColor("#66FCF1"),
Color.parseColor("#45B7AF"),
Shader.TileMode.CLAMP
)
blockPaint.shader = gradient
blockPaint.color = if (isGhost) Color.argb(30, 102, 252, 241) else Color.parseColor("#66FCF1")
blockPaint.alpha = if (isGhost) 30 else 255
blockPaint.style = Paint.Style.FILL
canvas.drawRect(left, top, right, bottom, blockPaint)
// Draw star-like sparkles
if (!isGhost) {
val sparklePaint = Paint().apply {
color = Color.WHITE
style = Paint.Style.FILL
isAntiAlias = true
maskFilter = BlurMaskFilter(4f, BlurMaskFilter.Blur.NORMAL)
}
// Add small white dots for sparkle effect
canvas.drawCircle(left + 4f, top + 4f, 1f, sparklePaint)
canvas.drawCircle(right - 4f, bottom - 4f, 1f, sparklePaint)
} }
} }
} }
// 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()
} }
/** /**
@ -1093,16 +695,7 @@ class GameView @JvmOverloads constructor(
// Custom touch event handling // Custom touch event handling
override fun onTouchEvent(event: MotionEvent): Boolean { override fun onTouchEvent(event: MotionEvent): Boolean {
if (!isRunning || isPaused || gameBoard.isGameOver) { if (!isRunning || isPaused) return false
return true
}
// Ignore touch events during the freeze period after a piece locks
val currentTime = System.currentTimeMillis()
if (currentTime < touchFreezeUntil) {
Log.d(TAG, "Ignoring touch event - freeze active for ${touchFreezeUntil - currentTime}ms more")
return true
}
when (event.action) { when (event.action) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
@ -1110,120 +703,53 @@ class GameView @JvmOverloads constructor(
startY = event.y startY = event.y
lastTouchX = event.x lastTouchX = event.x
lastTouchY = event.y lastTouchY = event.y
lastTapTime = currentTime // Set the tap time when touch starts lastTapTime = System.currentTimeMillis()
return true
// Reset direction lock
lockedDirection = null
} }
MotionEvent.ACTION_MOVE -> { MotionEvent.ACTION_MOVE -> {
val deltaX = event.x - lastTouchX val dx = event.x - lastTouchX
val deltaY = event.y - lastTouchY val dy = event.y - lastTouchY
// Check if we should lock direction // Horizontal movement
if (lockedDirection == null) { if (abs(dx) > blockSize / 2) {
val absDeltaX = abs(deltaX) if (dx > 0) {
val absDeltaY = abs(deltaY) movePieceRight()
} else {
if (absDeltaX > blockSize * directionLockThreshold || movePieceLeft()
absDeltaY > blockSize * directionLockThreshold) {
// Lock to the dominant direction
lockedDirection = if (absDeltaX > absDeltaY) {
Direction.HORIZONTAL
} else {
Direction.VERTICAL
}
} }
lastTouchX = event.x
} }
// Handle movement based on locked direction // Vertical movement (soft drop)
when (lockedDirection) { if (dy > blockSize / 2) {
Direction.HORIZONTAL -> { dropPiece()
if (abs(deltaX) > blockSize * minMovementThreshold) { lastTouchY = event.y
if (deltaX > 0) {
gameBoard.moveRight()
} else {
gameBoard.moveLeft()
}
lastTouchX = event.x
if (currentTime - lastMoveTime >= moveCooldown) {
gameHaptics?.vibrateForPieceMove()
lastMoveTime = currentTime
}
invalidate()
}
}
Direction.VERTICAL -> {
if (deltaY > blockSize * minMovementThreshold) {
gameBoard.softDrop()
lastTouchY = event.y
if (currentTime - lastMoveTime >= moveCooldown) {
gameHaptics?.vibrateForPieceMove()
lastMoveTime = currentTime
}
invalidate()
}
}
null -> {
// No direction lock yet, don't process movement
}
} }
return true
} }
MotionEvent.ACTION_UP -> { MotionEvent.ACTION_UP -> {
val deltaX = event.x - startX val dx = event.x - startX
val deltaY = event.y - startY val dy = event.y - startY
val moveTime = currentTime - lastTapTime val tapTime = System.currentTimeMillis() - lastTapTime
// Handle taps for rotation // Detect tap for rotation
if (moveTime < minTapTime * 1.5 && if (abs(dx) < blockSize / 2 && abs(dy) < blockSize / 2 && tapTime < 200) {
abs(deltaY) < maxTapMovement * 1.5 && rotatePiece()
abs(deltaX) < maxTapMovement * 1.5) {
if (currentTime - lastRotationTime >= rotationCooldown) {
gameBoard.rotate()
lastRotationTime = currentTime
gameHaptics?.vibrateForPieceMove()
invalidate()
}
return true
} }
// Handle gestures // Detect swipe down for hard drop
// Check for hold gesture (swipe up) if (dy > height / 4) {
if (deltaY < -blockSize * minHoldDistance && hardDrop()
abs(deltaX) / abs(deltaY) < 0.5f) {
if (currentTime - lastHoldTime >= holdCooldown) {
gameBoard.holdPiece()
lastHoldTime = currentTime
gameHaptics?.vibrateForPieceMove()
invalidate()
}
}
// Check for hard drop (must be faster and longer than soft drop)
else if (deltaY > blockSize * minHardDropDistance &&
abs(deltaX) / abs(deltaY) < 0.5f &&
(deltaY / moveTime) * 1000 > minSwipeVelocity) {
if (currentTime - lastHardDropTime >= hardDropCooldown) {
gameBoard.hardDrop()
lastHardDropTime = currentTime
invalidate()
}
}
// Check for soft drop (slower and shorter than hard drop)
else if (deltaY > blockSize * minMovementThreshold &&
deltaY < blockSize * maxSoftDropDistance &&
(deltaY / moveTime) * 1000 < minSwipeVelocity) {
gameBoard.softDrop()
invalidate()
} }
// Reset direction lock return true
lockedDirection = null
} }
} }
return true return false
} }
/** /**
@ -1249,7 +775,7 @@ class GameView @JvmOverloads constructor(
/** /**
* Get the next piece that will be spawned * Get the next piece that will be spawned
*/ */
fun getNextPiece(): Tetromino? { fun getNextPiece(): GamePiece? {
return gameBoard.getNextPiece() return gameBoard.getNextPiece()
} }
@ -1364,7 +890,7 @@ class GameView @JvmOverloads constructor(
// Create new animation // Create new animation
pulseAnimator = ValueAnimator.ofFloat(0f, 1f, 0f).apply { pulseAnimator = ValueAnimator.ofFloat(0f, 1f, 0f).apply {
duration = when (lineCount) { duration = when (lineCount) {
4 -> 2000L // Quad - longer duration 4 -> 2000L // Tetris - longer duration
3 -> 1600L // Triples 3 -> 1600L // Triples
2 -> 1200L // Doubles 2 -> 1200L // Doubles
1 -> 1000L // Singles 1 -> 1000L // Singles
@ -1609,4 +1135,103 @@ class GameView @JvmOverloads constructor(
gameHaptics?.vibrateForPieceMove() gameHaptics?.vibrateForPieceMove()
invalidate() invalidate()
} }
fun getNextPiece(): GamePiece? = nextPiece
fun getCurrentPiece(): GamePiece? = currentPiece
fun spawnNewPiece() {
currentPiece = gameBoard.spawnNewPiece()
nextPiece = gameBoard.getNextPiece()
onNextPieceChanged?.invoke()
}
fun rotatePiece() {
currentPiece?.let { piece ->
if (gameBoard.rotatePiece(piece)) {
invalidate()
}
}
}
fun movePieceLeft() {
currentPiece?.let { piece ->
if (gameBoard.movePiece(piece, -1, 0)) {
invalidate()
}
}
}
fun movePieceRight() {
currentPiece?.let { piece ->
if (gameBoard.movePiece(piece, 1, 0)) {
invalidate()
}
}
}
fun dropPiece() {
currentPiece?.let { piece ->
if (gameBoard.movePiece(piece, 0, 1)) {
invalidate()
}
}
}
fun hardDrop() {
currentPiece?.let { piece ->
val ghostY = calculateGhostY(piece)
if (ghostY > piece.y) {
piece.y = ghostY
gameBoard.lockPiece(piece)
spawnNewPiece()
invalidate()
}
}
}
fun startGame() {
if (!isRunning) {
isRunning = true
isPaused = false
gameBoard.reset()
spawnNewPiece()
handler.post(gameLoopRunnable)
}
}
fun pauseGame() {
if (isRunning && !isPaused) {
isPaused = true
handler.removeCallbacks(gameLoopRunnable)
}
}
fun resumeGame() {
if (isRunning && isPaused) {
isPaused = false
handler.post(gameLoopRunnable)
}
}
fun stopGame() {
isRunning = false
isPaused = false
handler.removeCallbacks(gameLoopRunnable)
}
fun resetGame() {
stopGame()
gameBoard.reset()
currentPiece = null
nextPiece = null
score = 0
invalidate()
}
fun getScore(): Int = score
fun setGameHaptics(haptics: GameHaptics?) {
gameHaptics = haptics
}
} }

View file

@ -46,51 +46,49 @@ class NextPieceView @JvmOverloads constructor(
super.onDraw(canvas) super.onDraw(canvas)
// Get the next piece from game view // Get the next piece from game view
gameView?.let { gameView?.getNextPiece()?.let { piece ->
it.getNextPiece()?.let { piece -> val width = piece.getWidth()
val width = piece.getWidth() val height = piece.getHeight()
val height = piece.getHeight()
// Calculate block size for the preview (smaller than main board)
// Calculate block size for the preview (smaller than main board) val previewBlockSize = min(
val previewBlockSize = min( canvas.width.toFloat() / (width.toFloat() + 2f),
canvas.width.toFloat() / (width + 2), canvas.height.toFloat() / (height.toFloat() + 2f)
canvas.height.toFloat() / (height + 2) )
)
// Center the piece in the preview area
// Center the piece in the preview area val previewLeft = (canvas.width.toFloat() - width.toFloat() * previewBlockSize) / 2f
val previewLeft = (canvas.width - width * previewBlockSize) / 2 val previewTop = (canvas.height.toFloat() - height.toFloat() * previewBlockSize) / 2f
val previewTop = (canvas.height - height * previewBlockSize) / 2
// Draw subtle background glow
// Draw subtle background glow val glowPaint = Paint().apply {
val glowPaint = Paint().apply { color = Color.WHITE
color = Color.WHITE alpha = 10
alpha = 10 maskFilter = BlurMaskFilter(previewBlockSize * 0.5f, BlurMaskFilter.Blur.OUTER)
maskFilter = BlurMaskFilter(previewBlockSize * 0.5f, BlurMaskFilter.Blur.OUTER) }
} canvas.drawRect(
canvas.drawRect( previewLeft - previewBlockSize,
previewLeft - previewBlockSize, previewTop - previewBlockSize,
previewTop - previewBlockSize, previewLeft + width.toFloat() * previewBlockSize + previewBlockSize,
previewLeft + width * previewBlockSize + previewBlockSize, previewTop + height.toFloat() * previewBlockSize + previewBlockSize,
previewTop + height * previewBlockSize + previewBlockSize, glowPaint
glowPaint )
)
for (y in 0 until height) {
for (y in 0 until height) { for (x in 0 until width) {
for (x in 0 until width) { if (piece.isBlockAt(x, y)) {
if (piece.isBlockAt(x, y)) { val left = previewLeft + x.toFloat() * previewBlockSize
val left = previewLeft + x * previewBlockSize val top = previewTop + y.toFloat() * previewBlockSize
val top = previewTop + y * previewBlockSize val right = left + previewBlockSize
val right = left + previewBlockSize val bottom = top + previewBlockSize
val bottom = top + previewBlockSize
// Draw block with subtle glow
// Draw block with subtle glow val rect = RectF(left + 1f, top + 1f, right - 1f, bottom - 1f)
val rect = RectF(left + 1, top + 1, right - 1, bottom - 1) canvas.drawRect(rect, blockPaint)
canvas.drawRect(rect, blockPaint)
// Draw subtle border glow
// Draw subtle border glow val glowRect = RectF(left, top, right, bottom)
val glowRect = RectF(left, top, right, bottom) canvas.drawRect(glowRect, glowPaint)
canvas.drawRect(glowRect, glowPaint)
}
} }
} }
} }

View file

@ -3,763 +3,197 @@ package com.pixelmintdrop.model
import android.util.Log import android.util.Log
/** /**
* Represents the game board (grid) and manages game state * Represents the game board and manages piece placement and collision detection
*/ */
class GameBoard( class GameBoard {
val width: Int = 10,
val height: Int = 20
) {
companion object { companion object {
private const val TAG = "GameBoard" const val WIDTH = 10
const val HEIGHT = 20
const val INITIAL_DROP_INTERVAL = 1000L // 1 second
const val MIN_DROP_INTERVAL = 100L // 0.1 seconds
} }
// Board grid to track locked pieces // Board state
// True = occupied, False = empty private val board = Array(HEIGHT) { Array(WIDTH) { false } }
private val grid = Array(height) { BooleanArray(width) { false } }
// Current active piece
private var currentPiece: GamePiece? = null private var currentPiece: GamePiece? = null
// Next piece to be played
private var nextPiece: GamePiece? = null private var nextPiece: GamePiece? = null
// Hold piece
private var holdPiece: GamePiece? = null
private var canHold = true
// 7-bag randomizer
private val bag = mutableListOf<GamePieceType>()
// Game state // Game state
var score = 0 var dropInterval = INITIAL_DROP_INTERVAL
var level = 1 private set
var startingLevel = 1 // Add this line to track the starting level
var lines = 0
var isGameOver = false
var isHardDropInProgress = false // Make public
var isPieceLocking = false // Make public
private var isPlayerSoftDrop = false // Track if the drop is player-initiated
private var lastLevel = 1 // Add this to track the previous level
// Scoring state val width: Int = WIDTH
private var combo = 0 val height: Int = HEIGHT
private var lastClearWasQuad = false
private var lastClearWasPerfect = false fun reset() {
private var lastClearWasAllClear = false // Clear the board
private var lastPieceClearedLines = false // Track if the last piece placed cleared lines for (y in 0 until HEIGHT) {
for (x in 0 until WIDTH) {
// Animation state board[y][x] = false
var linesToClear = mutableListOf<Int>()
var isLineClearAnimationInProgress = false
// Initial game speed (milliseconds per drop)
var dropInterval = 1000L
// Callbacks for game events
var onPieceMove: (() -> Unit)? = null
var onPieceLock: (() -> Unit)? = null
var onNextPieceChanged: (() -> Unit)? = null
var onLineClear: ((Int, List<Int>) -> Unit)? = null
var onPiecePlaced: (() -> Unit)? = null // New callback for when a piece is placed
// Store the last cleared lines
private val lastClearedLines = mutableListOf<Int>()
// Add spawn protection variables
private var pieceSpawnTime = 0L
private val spawnGracePeriod = 250L // Changed from 150ms to 250ms
init {
spawnNextPiece()
spawnPiece()
}
/**
* Generates the next tetromino piece using 7-bag randomizer
*/
private fun spawnNextPiece() {
// If bag is empty, refill it with all piece types
if (bag.isEmpty()) {
bag.addAll(GamePieceType.entries.toTypedArray())
bag.shuffle()
}
// Take the next piece from the bag
nextPiece = GamePiece(bag.removeAt(0))
onNextPieceChanged?.invoke()
}
/**
* Hold the current piece
*/
fun holdPiece() {
if (!canHold) return
val current = currentPiece
if (holdPiece == null) {
// If no piece is held, hold current piece and spawn new one
holdPiece = current
currentPiece = nextPiece
spawnNextPiece()
// Reset position of new piece
currentPiece?.apply {
x = (width - getWidth()) / 2
y = 0
}
} else {
// Swap current piece with held piece
currentPiece = holdPiece
holdPiece = current
// Reset position of swapped piece
currentPiece?.apply {
x = (width - getWidth()) / 2
y = 0
} }
} }
canHold = false
// Reset pieces
currentPiece = null
nextPiece = null
// Reset drop interval
dropInterval = INITIAL_DROP_INTERVAL
} }
/** fun spawnNewPiece(): GamePiece {
* Get the currently held piece // Get the next piece or create a new one if none exists
*/ currentPiece = nextPiece ?: createRandomPiece()
fun getHoldPiece(): GamePiece? = holdPiece nextPiece = createRandomPiece()
/** // Position the piece at the top center of the board
* Get the next piece that will be spawned currentPiece?.let { piece ->
*/ piece.x = (WIDTH - piece.getWidth()) / 2
piece.y = 0
}
return currentPiece!!
}
fun getNextPiece(): GamePiece? = nextPiece fun getNextPiece(): GamePiece? = nextPiece
/** fun getCurrentPiece(): GamePiece? = currentPiece
* Spawns the current tetromino at the top of the board
*/ fun hasBlockAt(x: Int, y: Int): Boolean {
fun spawnPiece() { if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) return false
Log.d(TAG, "spawnPiece() started - current states: isHardDropInProgress=$isHardDropInProgress, isPieceLocking=$isPieceLocking") return board[y][x]
currentPiece = nextPiece
spawnNextPiece()
// Center the piece horizontally and spawn one unit higher
currentPiece?.apply {
x = (width - getWidth()) / 2
y = -1 // Spawn one unit above the top of the screen
Log.d(TAG, "spawnPiece() - new piece spawned at position (${x},${y}), type=${type}")
// Set the spawn time for the grace period
pieceSpawnTime = System.currentTimeMillis()
// Check if the piece can be placed (Game Over condition)
if (!canMove(0, 0)) {
isGameOver = true
Log.d(TAG, "spawnPiece() - Game Over condition detected")
}
}
} }
/** fun wouldCollide(piece: GamePiece, newX: Int, newY: Int): Boolean {
* Move the current piece left val width = piece.getWidth()
*/ val height = piece.getHeight()
fun moveLeft() {
if (canMove(-1, 0)) {
currentPiece?.x = currentPiece?.x?.minus(1) ?: 0
onPieceMove?.invoke()
}
}
/**
* Move the current piece right
*/
fun moveRight() {
if (canMove(1, 0)) {
currentPiece?.x = currentPiece?.x?.plus(1) ?: 0
onPieceMove?.invoke()
}
}
/**
* Move the current piece down (soft drop)
*/
fun moveDown(): Boolean {
// Don't allow movement if a hard drop is in progress or piece is locking
if (isHardDropInProgress || isPieceLocking) return false
return if (canMove(0, 1)) { for (y in 0 until height) {
currentPiece?.y = currentPiece?.y?.plus(1) ?: 0 for (x in 0 until width) {
// Only add soft drop points if it's a player-initiated drop
if (isPlayerSoftDrop) {
score += 1
}
onPieceMove?.invoke()
true
} else {
// Check if we're within the spawn grace period
val currentTime = System.currentTimeMillis()
if (currentTime - pieceSpawnTime < spawnGracePeriod) {
Log.d(TAG, "moveDown() - not locking piece due to spawn grace period (${currentTime - pieceSpawnTime}ms < ${spawnGracePeriod}ms)")
return false
}
lockPiece()
false
}
}
/**
* Player-initiated soft drop
*/
fun softDrop() {
isPlayerSoftDrop = true
moveDown()
isPlayerSoftDrop = false
}
/**
* Hard drop the current piece
*/
fun hardDrop() {
if (isHardDropInProgress || isPieceLocking) {
Log.d(TAG, "hardDrop() called but blocked: isHardDropInProgress=$isHardDropInProgress, isPieceLocking=$isPieceLocking")
return // Prevent multiple hard drops
}
// Check if we're within the spawn grace period
val currentTime = System.currentTimeMillis()
if (currentTime - pieceSpawnTime < spawnGracePeriod) {
Log.d(TAG, "hardDrop() - blocked due to spawn grace period (${currentTime - pieceSpawnTime}ms < ${spawnGracePeriod}ms)")
return
}
Log.d(TAG, "hardDrop() started - setting isHardDropInProgress=true")
isHardDropInProgress = true
val piece = currentPiece ?: return
// Count how many cells the piece will drop
var dropDistance = 0
while (canMove(0, dropDistance + 1)) {
dropDistance++
}
Log.d(TAG, "hardDrop() - piece will drop $dropDistance cells, position before: (${piece.x},${piece.y})")
// Move piece down until it can't move anymore
while (canMove(0, 1)) {
piece.y++
onPieceMove?.invoke()
}
Log.d(TAG, "hardDrop() - piece final position: (${piece.x},${piece.y})")
// Add hard drop points (2 points per cell)
score += dropDistance * 2
// Lock the piece immediately
lockPiece()
}
/**
* Rotate the current piece clockwise
*/
fun rotate() {
currentPiece?.let {
// Save current rotation
val originalX = it.x
val originalY = it.y
// Try to rotate
it.rotateClockwise()
// Wall kick logic - try to move the piece if rotation causes collision
if (!canMove(0, 0)) {
// Try to move left
if (canMove(-1, 0)) {
it.x--
}
// Try to move right
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.rotateCounterClockwise()
it.x = originalX
it.y = originalY
}
}
}
}
/**
* Rotate the current piece counterclockwise
*/
fun rotateCounterClockwise() {
currentPiece?.let {
// Save current rotation
val originalX = it.x
val originalY = it.y
// Try to rotate
it.rotateCounterClockwise()
// Wall kick logic - try to move the piece if rotation causes collision
if (!canMove(0, 0)) {
// Try to move left
if (canMove(-1, 0)) {
it.x--
}
// Try to move right
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
}
}
}
}
/**
* Check if the current piece can move to the given position
*/
fun canMove(deltaX: Int, deltaY: Int): Boolean {
val piece = currentPiece ?: return false
val newX = piece.x + deltaX
val newY = piece.y + deltaY
for (y in 0 until piece.getHeight()) {
for (x in 0 until piece.getWidth()) {
if (piece.isBlockAt(x, y)) { if (piece.isBlockAt(x, y)) {
val boardX = newX + x val boardX = newX + x
val boardY = newY + y val boardY = newY + y
// Check if the position is outside the board horizontally // Check board boundaries
if (boardX < 0 || boardX >= width) { if (boardX < 0 || boardX >= WIDTH || boardY >= HEIGHT) {
return false return true
} }
// Check if the position is below the board // Check collision with placed blocks
if (boardY >= height) { if (boardY >= 0 && board[boardY][boardX]) {
return false return true
}
// Check if the position is already occupied (but not if it's above the board)
if (boardY >= 0 && grid[boardY][boardX]) {
return false
}
// Check if the position is more than one unit above the top of the screen
if (boardY < -1) {
return false
} }
} }
} }
} }
return false
}
fun movePiece(piece: GamePiece, dx: Int, dy: Int): Boolean {
val newX = piece.x + dx
val newY = piece.y + dy
if (!wouldCollide(piece, newX, newY)) {
piece.x = newX
piece.y = newY
return true
}
return false
}
fun rotatePiece(piece: GamePiece): Boolean {
// Save current state
val originalRotation = piece.rotation
// Try to rotate
piece.rotate()
// Check if new position is valid
if (wouldCollide(piece, piece.x, piece.y)) {
// If not valid, try wall kicks
val kicks = arrayOf(
Pair(1, 0), // Try moving right
Pair(-1, 0), // Try moving left
Pair(0, -1), // Try moving up
Pair(2, 0), // Try moving right 2
Pair(-2, 0) // Try moving left 2
)
for ((kickX, kickY) in kicks) {
if (!wouldCollide(piece, piece.x + kickX, piece.y + kickY)) {
piece.x += kickX
piece.y += kickY
return true
}
}
// If no wall kick worked, revert rotation
piece.rotation = originalRotation
return false
}
return true return true
} }
/** fun lockPiece(piece: GamePiece) {
* Lock the current piece in place val width = piece.getWidth()
*/ val height = piece.getHeight()
private fun lockPiece() {
if (isPieceLocking) {
Log.d(TAG, "lockPiece() called but blocked: isPieceLocking=$isPieceLocking")
return // Prevent recursive locking
}
Log.d(TAG, "lockPiece() started - setting isPieceLocking=true, current isHardDropInProgress=$isHardDropInProgress") // Place the piece on the board
isPieceLocking = true for (y in 0 until height) {
for (x in 0 until width) {
val piece = currentPiece ?: return
// Add the piece to the grid
for (y in 0 until piece.getHeight()) {
for (x in 0 until piece.getWidth()) {
if (piece.isBlockAt(x, y)) { if (piece.isBlockAt(x, y)) {
val boardX = piece.x + x val boardX = piece.x + x
val boardY = piece.y + y val boardY = piece.y + y
if (boardY >= 0 && boardY < HEIGHT && boardX >= 0 && boardX < WIDTH) {
// Only add to grid if within bounds board[boardY][boardX] = true
if (boardY >= 0 && boardY < height && boardX >= 0 && boardX < width) {
grid[boardY][boardX] = true
} }
} }
} }
} }
// Trigger the piece lock vibration // Clear any completed lines
onPieceLock?.invoke() clearLines()
// Notify that a piece was placed
onPiecePlaced?.invoke()
// Find and clear lines immediately
findAndClearLines()
// IMPORTANT: Reset the hard drop flag before spawning a new piece
// This prevents the immediate hard drop of the next piece
if (isHardDropInProgress) {
Log.d(TAG, "lockPiece() - resetting isHardDropInProgress=false BEFORE spawning new piece")
isHardDropInProgress = false
}
// Log piece position before spawning new piece
Log.d(TAG, "lockPiece() - about to spawn new piece at y=${piece.y}, isHardDropInProgress=$isHardDropInProgress")
// Spawn new piece immediately
spawnPiece()
// Allow holding piece again after locking
canHold = true
// Reset locking state
isPieceLocking = false
Log.d(TAG, "lockPiece() completed - reset flags: isPieceLocking=false, isHardDropInProgress=$isHardDropInProgress")
} }
/** private fun clearLines() {
* Find and clear completed lines immediately var linesCleared = 0
*/ var y = HEIGHT - 1
private fun findAndClearLines() {
// Quick scan for completed lines
var shiftAmount = 0
var y = height - 1
val linesToClear = mutableListOf<Int>()
while (y >= 0) { while (y >= 0) {
if (grid[y].all { it }) { if (isLineFull(y)) {
// Line is full, add to lines to clear // Move all lines above down
linesToClear.add(y) for (moveY in y downTo 1) {
shiftAmount++ for (x in 0 until WIDTH) {
} else if (shiftAmount > 0) { board[moveY][x] = board[moveY - 1][x]
// Shift this row down by shiftAmount }
System.arraycopy(grid[y], 0, grid[y + shiftAmount], 0, width)
}
y--
}
// Store the last cleared lines
lastClearedLines.clear()
lastClearedLines.addAll(linesToClear)
// If lines were cleared, calculate score in background and trigger callback
if (shiftAmount > 0) {
// Log line clear
Log.d(TAG, "Lines cleared: $shiftAmount")
// Trigger line clear callback on main thread with the lines that were cleared
val mainHandler = android.os.Handler(android.os.Looper.getMainLooper())
mainHandler.post {
// Call the line clear callback with the cleared line count
try {
Log.d(TAG, "Triggering onLineClear callback with $shiftAmount lines")
val clearedLines = getLastClearedLines()
onLineClear?.invoke(shiftAmount, clearedLines)
Log.d(TAG, "onLineClear callback completed successfully")
} catch (e: Exception) {
Log.e(TAG, "Error in onLineClear callback", e)
} }
} // Clear top line
for (x in 0 until WIDTH) {
// Clear top rows after callback board[0][x] = false
for (y in 0 until shiftAmount) { }
java.util.Arrays.fill(grid[y], false) linesCleared++
}
Thread {
calculateScore(shiftAmount)
}.start()
}
// Update combo based on whether this piece cleared lines
if (shiftAmount > 0) {
if (lastPieceClearedLines) {
combo++
} else { } else {
combo = 1 // Start new combo y--
}
} else {
combo = 0 // Reset combo if no lines cleared
}
lastPieceClearedLines = shiftAmount > 0
}
/**
* Calculate score for cleared lines
*/
private fun calculateScore(clearedLines: Int) {
// Pre-calculated score multipliers for better performance
val baseScore = when (clearedLines) {
1 -> 40
2 -> 100
3 -> 300
4 -> 1200
else -> 0
}
// Check for perfect clear (no blocks left)
val isPerfectClear = !grid.any { row -> row.any { it } }
// Check for all clear (no blocks in playfield)
val isAllClear = !grid.any { row -> row.any { it } } &&
currentPiece == null &&
nextPiece == null
// Calculate combo multiplier
val comboMultiplier = if (combo > 0) {
when (combo) {
1 -> 1.0
2 -> 1.5
3 -> 2.0
4 -> 2.5
else -> 3.0
}
} else 1.0
// Calculate back-to-back quad bonus
val backToBackMultiplier = if (clearedLines == 4 && lastClearWasQuad) 1.5 else 1.0
// Calculate perfect clear bonus
val perfectClearMultiplier = if (isPerfectClear) {
when (clearedLines) {
1 -> 2.0
2 -> 3.0
3 -> 4.0
4 -> 5.0
else -> 1.0
}
} else 1.0
// Calculate all clear bonus
val allClearMultiplier = if (isAllClear) 2.0 else 1.0
// Calculate T-Spin bonus
val tSpinMultiplier = if (isTSpin()) {
when (clearedLines) {
1 -> 2.0
2 -> 4.0
3 -> 6.0
else -> 1.0
}
} else 1.0
// Calculate final score with all multipliers
val finalScore = (baseScore * level * comboMultiplier *
backToBackMultiplier * perfectClearMultiplier *
allClearMultiplier * tSpinMultiplier).toInt()
// Update score on main thread
Thread {
score += finalScore
}.start()
// Update line clear state
lastClearWasQuad = clearedLines == 4
lastClearWasPerfect = isPerfectClear
lastClearWasAllClear = isAllClear
// Update lines cleared and level
lines += clearedLines
// Calculate level based on lines cleared, but ensure it's never below the starting level
level = Math.max((lines / 10) + 1, startingLevel)
// Update game speed based on level (NES formula)
dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong()
}
/**
* Check if the last move was a T-Spin
*/
private fun isTSpin(): Boolean {
val piece = currentPiece ?: return false
if (piece.type != GamePieceType.T) return false
// Count occupied corners around the T piece
var occupiedCorners = 0
val centerX = piece.x + 1
val centerY = piece.y + 1
// Check all four corners
if (isOccupied(centerX - 1, centerY - 1)) occupiedCorners++
if (isOccupied(centerX + 1, centerY - 1)) occupiedCorners++
if (isOccupied(centerX - 1, centerY + 1)) occupiedCorners++
if (isOccupied(centerX + 1, centerY + 1)) occupiedCorners++
// T-Spin requires at least 3 occupied corners
return occupiedCorners >= 3
}
/**
* Get the ghost piece position (preview of where piece will land)
*/
fun getGhostY(): Int {
val piece = currentPiece ?: return 0
var ghostY = piece.y
// Find how far the piece can move down
while (true) {
if (canMove(0, ghostY - piece.y + 1)) {
ghostY++
} else {
break
} }
} }
// Ensure ghostY doesn't exceed the board height // Increase speed based on lines cleared
return ghostY.coerceAtMost(height - 1) if (linesCleared > 0) {
} dropInterval = (dropInterval * 0.95).toLong().coerceAtLeast(MIN_DROP_INTERVAL)
/**
* Get the current tetromino
*/
fun getCurrentPiece(): GamePiece? = currentPiece
/**
* Check if a cell in the grid is occupied
*/
fun isOccupied(x: Int, y: Int): Boolean {
return if (x in 0 until width && y in 0 until height) {
grid[y][x]
} else {
false
}
}
/**
* 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
*/
fun updateLevel(newLevel: Int) {
lastLevel = level
level = newLevel.coerceIn(1, 20)
startingLevel = level // Store the starting level
dropInterval = getDropIntervalForLevel(level)
}
/**
* Start a new game
*/
fun startGame() {
reset()
// Initialize pieces
spawnNextPiece()
spawnPiece()
}
/**
* Reset the game board
*/
fun reset() {
// Clear the grid
for (y in 0 until height) {
for (x in 0 until width) {
grid[y][x] = false
}
}
// Reset game state
score = 0
level = startingLevel // Use starting level instead of resetting to 1
lastLevel = level // Reset lastLevel to match the current level
lines = 0
isGameOver = false
dropInterval = getDropIntervalForLevel(level) // Use helper method
// Reset scoring state
combo = 0
lastClearWasQuad = false
lastClearWasPerfect = false
lastClearWasAllClear = false
lastPieceClearedLines = false
// Reset piece state
holdPiece = null
canHold = true
bag.clear()
// Clear current and next pieces
currentPiece = null
nextPiece = null
}
/**
* Clear completed lines and move blocks down (legacy method, kept for reference)
*/
private fun clearLines(): Int {
return linesToClear.size // Return the number of lines that will be cleared
}
/**
* Get the current combo count
*/
fun getCombo(): Int = combo
/**
* Get the list of lines that were most recently cleared
*/
private fun getLastClearedLines(): List<Int> {
return lastClearedLines.toList()
}
/**
* Get the last level
*/
fun getLastLevel(): Int = lastLevel
/**
* Update the game state (called by game loop)
*/
fun update() {
if (!isGameOver) {
moveDown()
} }
} }
/** private fun isLineFull(y: Int): Boolean {
* Get the drop interval for the given level for (x in 0 until WIDTH) {
*/ if (!board[y][x]) return false
private fun getDropIntervalForLevel(level: Int): Long { }
val cappedLevel = level.coerceIn(1, 20) return true
// Update game speed based on level (NES formula)
return (1000 * Math.pow(0.8, (cappedLevel - 1).toDouble())).toLong()
} }
/** private fun createRandomPiece(): GamePiece {
* Update the game level return GamePiece(GamePieceType.values().random())
*/ }
} }

View file

@ -1,214 +1,48 @@
package com.pixelmintdrop.model package com.pixelmintdrop.model
/** /**
* Represents a game piece * Represents a game piece with its type, position, and rotation
*/ */
enum class GamePieceType {
I, J, L, O, S, T, Z
}
class GamePiece(val type: GamePieceType) { class GamePiece(val type: GamePieceType) {
private var rotation = 0 var x: Int = 0
var y: Int = 0
var rotation: Int = 0
private set
// Each piece has 4 rotations (0, 90, 180, 270 degrees) private val blocks: Array<Array<Boolean>> = type.getBlocks()
private val blocks: Array<Array<BooleanArray>> = getBlocks(type)
/** fun getWidth(): Int = blocks[0].size
* Get the current shape of the piece based on rotation
*/
fun getShape(): Array<BooleanArray> = blocks[rotation]
/** fun getHeight(): Int = blocks.size
* Get the width of the current piece shape
*/
fun getWidth(): Int = blocks[rotation][0].size
/** fun isBlockAt(x: Int, y: Int): Boolean {
* Get the height of the current piece shape
*/
fun getHeight(): Int = blocks[rotation].size
/**
* Rotate the piece clockwise
*/
fun rotateClockwise() {
rotation = (rotation + 1) % 4
}
/**
* Rotate the piece counter-clockwise
*/
fun rotateCounterClockwise() {
rotation = (rotation + 3) % 4
}
/**
* Check if the piece's block exists at the given coordinates
*/
fun hasBlock(x: Int, y: Int): Boolean {
if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight()) return false if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight()) return false
return blocks[rotation][y][x] return blocks[y][x]
} }
/** fun rotate() {
* Get the block patterns for each piece type and all its rotations rotation = (rotation + 1) % 4
*/ rotateBlocks()
private fun getBlocks(type: GamePieceType): Array<Array<BooleanArray>> { }
return when (type) {
GamePieceType.I -> arrayOf( private fun rotateBlocks() {
arrayOf( val width = getWidth()
booleanArrayOf(false, false, false, false), val height = getHeight()
booleanArrayOf(true, true, true, true), val rotated = Array(width) { Array(height) { false } }
booleanArrayOf(false, false, false, false),
booleanArrayOf(false, false, false, false) for (y in 0 until height) {
), for (x in 0 until width) {
arrayOf( when (rotation) {
booleanArrayOf(false, false, true, false), 1 -> rotated[x][height - 1 - y] = blocks[y][x]
booleanArrayOf(false, false, true, false), 2 -> rotated[height - 1 - y][width - 1 - x] = blocks[y][x]
booleanArrayOf(false, false, true, false), 3 -> rotated[width - 1 - x][y] = blocks[y][x]
booleanArrayOf(false, false, true, false) else -> rotated[y][x] = blocks[y][x]
), }
arrayOf( }
booleanArrayOf(false, false, false, false), }
booleanArrayOf(false, false, false, false),
booleanArrayOf(true, true, true, true), blocks.indices.forEach { i ->
booleanArrayOf(false, false, false, false) blocks[i] = rotated[i].copyOf()
),
arrayOf(
booleanArrayOf(false, true, false, false),
booleanArrayOf(false, true, false, false),
booleanArrayOf(false, true, false, false),
booleanArrayOf(false, true, false, false)
)
),
GamePieceType.J -> arrayOf(
arrayOf(
booleanArrayOf(true, false, false),
booleanArrayOf(true, true, true),
booleanArrayOf(false, false, false)
),
arrayOf(
booleanArrayOf(false, true, true),
booleanArrayOf(false, true, false),
booleanArrayOf(false, true, false)
),
arrayOf(
booleanArrayOf(false, false, false),
booleanArrayOf(true, true, true),
booleanArrayOf(false, false, true)
),
arrayOf(
booleanArrayOf(false, true, false),
booleanArrayOf(false, true, false),
booleanArrayOf(true, true, false)
)
),
GamePieceType.L -> arrayOf(
arrayOf(
booleanArrayOf(false, false, true),
booleanArrayOf(true, true, true),
booleanArrayOf(false, false, false)
),
arrayOf(
booleanArrayOf(false, true, false),
booleanArrayOf(false, true, false),
booleanArrayOf(false, true, true)
),
arrayOf(
booleanArrayOf(false, false, false),
booleanArrayOf(true, true, true),
booleanArrayOf(true, false, false)
),
arrayOf(
booleanArrayOf(true, true, false),
booleanArrayOf(false, true, false),
booleanArrayOf(false, true, false)
)
),
GamePieceType.O -> arrayOf(
arrayOf(
booleanArrayOf(true, true),
booleanArrayOf(true, true)
),
arrayOf(
booleanArrayOf(true, true),
booleanArrayOf(true, true)
),
arrayOf(
booleanArrayOf(true, true),
booleanArrayOf(true, true)
),
arrayOf(
booleanArrayOf(true, true),
booleanArrayOf(true, true)
)
),
GamePieceType.S -> arrayOf(
arrayOf(
booleanArrayOf(false, true, true),
booleanArrayOf(true, true, false),
booleanArrayOf(false, false, false)
),
arrayOf(
booleanArrayOf(false, true, false),
booleanArrayOf(false, true, true),
booleanArrayOf(false, false, true)
),
arrayOf(
booleanArrayOf(false, false, false),
booleanArrayOf(false, true, true),
booleanArrayOf(true, true, false)
),
arrayOf(
booleanArrayOf(true, false, false),
booleanArrayOf(true, true, false),
booleanArrayOf(false, true, false)
)
),
GamePieceType.T -> arrayOf(
arrayOf(
booleanArrayOf(false, true, false),
booleanArrayOf(true, true, true),
booleanArrayOf(false, false, false)
),
arrayOf(
booleanArrayOf(false, true, false),
booleanArrayOf(false, true, true),
booleanArrayOf(false, true, false)
),
arrayOf(
booleanArrayOf(false, false, false),
booleanArrayOf(true, true, true),
booleanArrayOf(false, true, false)
),
arrayOf(
booleanArrayOf(false, true, false),
booleanArrayOf(true, true, false),
booleanArrayOf(false, true, false)
)
),
GamePieceType.Z -> arrayOf(
arrayOf(
booleanArrayOf(true, true, false),
booleanArrayOf(false, true, true),
booleanArrayOf(false, false, false)
),
arrayOf(
booleanArrayOf(false, false, true),
booleanArrayOf(false, true, true),
booleanArrayOf(false, true, false)
),
arrayOf(
booleanArrayOf(false, false, false),
booleanArrayOf(true, true, false),
booleanArrayOf(false, true, true)
),
arrayOf(
booleanArrayOf(false, true, false),
booleanArrayOf(true, true, false),
booleanArrayOf(true, false, false)
)
)
} }
} }
} }

View file

@ -0,0 +1,48 @@
package com.pixelmintdrop.model
/**
* Represents the different types of game pieces
*/
enum class GamePieceType {
I, J, L, O, S, T, Z;
fun getBlocks(): Array<Array<Boolean>> {
return when (this) {
I -> arrayOf(
arrayOf(false, false, false, false),
arrayOf(true, true, true, true),
arrayOf(false, false, false, false),
arrayOf(false, false, false, false)
)
J -> arrayOf(
arrayOf(true, false, false),
arrayOf(true, true, true),
arrayOf(false, false, false)
)
L -> arrayOf(
arrayOf(false, false, true),
arrayOf(true, true, true),
arrayOf(false, false, false)
)
O -> arrayOf(
arrayOf(true, true),
arrayOf(true, true)
)
S -> arrayOf(
arrayOf(false, true, true),
arrayOf(true, true, false),
arrayOf(false, false, false)
)
T -> arrayOf(
arrayOf(false, true, false),
arrayOf(true, true, true),
arrayOf(false, false, false)
)
Z -> arrayOf(
arrayOf(true, true, false),
arrayOf(false, true, true),
arrayOf(false, false, false)
)
}
}
}

View file

@ -93,15 +93,31 @@ class PlayerProgressionManager(context: Context) {
/** /**
* Calculate XP from a game session based on score, lines, level, etc. * Calculate XP from a game session based on score, lines, level, etc.
*/ */
fun calculateGameXP(score: Int, lines: Int, level: Int, timePlayedMs: Long, fun calculateGameXP(
quadCount: Int, perfectClearCount: Int): Long { score: Int,
val scoreXP = (score * SCORE_XP_MULTIPLIER).toLong() lines: Int,
val linesXP = (lines * LINES_XP_MULTIPLIER).toLong() level: Int,
val quadBonus = (quadCount * QUAD_XP_BONUS).toLong() timePlayedMs: Long,
val perfectClearBonus = (perfectClearCount * 100).toLong() quadCount: Int,
val timeBonus = (timePlayedMs * TIME_XP_MULTIPLIER).toLong() perfectClearCount: Int
): Long {
// Calculate base XP from score
val scoreXP = score * BASE_SCORE_XP * level
return scoreXP + linesXP + quadBonus + perfectClearBonus + timeBonus // Calculate XP from lines cleared
val linesXP = lines * BASE_LINES_XP * level
// Calculate quad bonus
val quadBonus = quadCount * BASE_QUAD_BONUS * level
// Calculate perfect clear bonus
val perfectClearBonus = perfectClearCount * BASE_PERFECT_CLEAR_BONUS * level
// Calculate time bonus (convert ms to seconds)
val timeBonus = (timePlayedMs / 1000.0) * BASE_TIME_XP * level
// Sum all XP components
return (scoreXP + linesXP + quadBonus + perfectClearBonus + timeBonus).toLong()
} }
/** /**
@ -319,10 +335,11 @@ class PlayerProgressionManager(context: Context) {
THEME_GALAXY to 25 THEME_GALAXY to 25
) )
private const val SCORE_XP_MULTIPLIER = 0.1 private const val BASE_SCORE_XP = 0.1 // XP per score point
private const val LINES_XP_MULTIPLIER = 5 private const val BASE_LINES_XP = 100.0 // XP per line cleared
private const val QUAD_XP_BONUS = 50 private const val BASE_QUAD_BONUS = 500.0 // Bonus XP for clearing 4 lines at once
private const val TIME_XP_MULTIPLIER = 0.01 private const val BASE_PERFECT_CLEAR_BONUS = 1000.0 // Bonus XP for perfect clear
private const val BASE_TIME_XP = 1.0 // XP per second played
} }
/** /**

View file

@ -3,211 +3,76 @@ package com.pixelmintdrop.model
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
/**
* Manages game statistics and high scores
*/
class StatsManager(context: Context) { class StatsManager(context: Context) {
companion object {
private const val PREFS_NAME = "game_stats"
private const val KEY_HIGH_SCORE = "high_score"
private const val KEY_TOTAL_GAMES = "total_games"
private const val KEY_TOTAL_LINES = "total_lines"
private const val KEY_TOTAL_QUADS = "total_quads"
private const val KEY_TOTAL_PERFECT_CLEARS = "total_perfect_clears"
private const val KEY_SESSION_SCORE = "session_score"
private const val KEY_SESSION_LINES = "session_lines"
private const val KEY_SESSION_QUADS = "session_quads"
private const val KEY_SESSION_PERFECT_CLEARS = "session_perfect_clears"
}
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
// Lifetime stats
private var totalGames: Int = 0
private var totalScore: Long = 0
private var totalLines: Int = 0
private var totalPieces: Int = 0
private var totalTime: Long = 0
private var maxLevel: Int = 0
private var maxScore: Int = 0
private var maxLines: Int = 0
// Line clear stats (lifetime)
private var totalSingles: Int = 0
private var totalDoubles: Int = 0
private var totalTriples: Int = 0
private var totalQuads: Int = 0
// Session stats // Session stats
private var sessionScore: Int = 0 private var sessionScore = 0
private var sessionLines: Int = 0 private var sessionLines = 0
private var sessionPieces: Int = 0 private var sessionQuads = 0
private var sessionTime: Long = 0 private var sessionPerfectClears = 0
private var sessionLevel: Int = 0
fun getHighScore(): Int = prefs.getInt(KEY_HIGH_SCORE, 0)
// Line clear stats (session) fun getTotalGames(): Int = prefs.getInt(KEY_TOTAL_GAMES, 0)
private var sessionSingles: Int = 0 fun getTotalLines(): Int = prefs.getInt(KEY_TOTAL_LINES, 0)
private var sessionDoubles: Int = 0 fun getTotalQuads(): Int = prefs.getInt(KEY_TOTAL_QUADS, 0)
private var sessionTriples: Int = 0 fun getTotalPerfectClears(): Int = prefs.getInt(KEY_TOTAL_PERFECT_CLEARS, 0)
private var sessionQuads: Int = 0
fun getSessionScore(): Int = sessionScore
// Perfect clear stats fun getSessionLines(): Int = sessionLines
private var sessionPerfectClears: Int = 0 fun getSessionQuads(): Int = sessionQuads
private var totalPerfectClears: Int = 0 fun getSessionPerfectClears(): Int = sessionPerfectClears
init { fun updateStats(score: Int, lines: Int, isQuad: Boolean, isPerfectClear: Boolean) {
loadStats() // Update session stats
} sessionScore += score
sessionLines += lines
private fun loadStats() { if (isQuad) sessionQuads++
totalGames = prefs.getInt(KEY_TOTAL_GAMES, 0) if (isPerfectClear) sessionPerfectClears++
totalScore = prefs.getLong(KEY_TOTAL_SCORE, 0)
totalLines = prefs.getInt(KEY_TOTAL_LINES, 0) // Update high score if needed
totalPieces = prefs.getInt(KEY_TOTAL_PIECES, 0) if (sessionScore > getHighScore()) {
totalTime = prefs.getLong(KEY_TOTAL_TIME, 0) prefs.edit().putInt(KEY_HIGH_SCORE, sessionScore).apply()
maxLevel = prefs.getInt(KEY_MAX_LEVEL, 0) }
maxScore = prefs.getInt(KEY_MAX_SCORE, 0)
maxLines = prefs.getInt(KEY_MAX_LINES, 0) // Update total stats
// Load line clear stats
totalSingles = prefs.getInt(KEY_TOTAL_SINGLES, 0)
totalDoubles = prefs.getInt(KEY_TOTAL_DOUBLES, 0)
totalTriples = prefs.getInt(KEY_TOTAL_TRIPLES, 0)
totalQuads = prefs.getInt(KEY_TOTAL_QUADS, 0)
// Load perfect clear stats
totalPerfectClears = prefs.getInt(KEY_TOTAL_PERFECT_CLEARS, 0)
}
private fun saveStats() {
prefs.edit() prefs.edit()
.putInt(KEY_TOTAL_GAMES, totalGames) .putInt(KEY_TOTAL_LINES, getTotalLines() + lines)
.putLong(KEY_TOTAL_SCORE, totalScore) .putInt(KEY_TOTAL_QUADS, getTotalQuads() + if (isQuad) 1 else 0)
.putInt(KEY_TOTAL_LINES, totalLines) .putInt(KEY_TOTAL_PERFECT_CLEARS, getTotalPerfectClears() + if (isPerfectClear) 1 else 0)
.putInt(KEY_TOTAL_PIECES, totalPieces)
.putLong(KEY_TOTAL_TIME, totalTime)
.putInt(KEY_MAX_LEVEL, maxLevel)
.putInt(KEY_MAX_SCORE, maxScore)
.putInt(KEY_MAX_LINES, maxLines)
.putInt(KEY_TOTAL_SINGLES, totalSingles)
.putInt(KEY_TOTAL_DOUBLES, totalDoubles)
.putInt(KEY_TOTAL_TRIPLES, totalTriples)
.putInt(KEY_TOTAL_QUADS, totalQuads)
.putInt(KEY_TOTAL_PERFECT_CLEARS, totalPerfectClears)
.apply() .apply()
} }
fun startNewSession() { fun startNewGame() {
// Increment total games counter
prefs.edit().putInt(KEY_TOTAL_GAMES, getTotalGames() + 1).apply()
}
fun resetSession() {
sessionScore = 0 sessionScore = 0
sessionLines = 0 sessionLines = 0
sessionPieces = 0
sessionTime = 0
sessionLevel = 0
sessionSingles = 0
sessionDoubles = 0
sessionTriples = 0
sessionQuads = 0 sessionQuads = 0
sessionPerfectClears = 0 sessionPerfectClears = 0
} }
fun updateSessionStats(score: Int, lines: Int, pieces: Int, time: Long, level: Int) { fun resetAllStats() {
sessionScore = score prefs.edit().clear().apply()
sessionLines = lines resetSession()
sessionPieces = pieces
sessionTime = time
sessionLevel = level
}
fun recordLineClear(lineCount: Int) {
when (lineCount) {
1 -> {
sessionSingles++
totalSingles++
}
2 -> {
sessionDoubles++
totalDoubles++
}
3 -> {
sessionTriples++
totalTriples++
}
4 -> {
sessionQuads++
totalQuads++
}
}
}
fun endSession() {
totalGames++
totalScore += sessionScore
totalLines += sessionLines
totalPieces += sessionPieces
totalTime += sessionTime
if (sessionLevel > maxLevel) maxLevel = sessionLevel
if (sessionScore > maxScore) maxScore = sessionScore
if (sessionLines > maxLines) maxLines = sessionLines
saveStats()
}
// Getters for lifetime stats
fun getTotalGames(): Int = totalGames
fun getTotalScore(): Long = totalScore
fun getTotalLines(): Int = totalLines
fun getTotalPieces(): Int = totalPieces
fun getTotalTime(): Long = totalTime
fun getMaxLevel(): Int = maxLevel
fun getMaxScore(): Int = maxScore
fun getMaxLines(): Int = maxLines
// Getters for line clear stats (lifetime)
fun getTotalSingles(): Int = totalSingles
fun getTotalDoubles(): Int = totalDoubles
fun getTotalTriples(): Int = totalTriples
fun getTotalQuads(): Int = totalQuads
// Getters for session stats
fun getSessionScore(): Int = sessionScore
fun getSessionLines(): Int = sessionLines
fun getSessionPieces(): Int = sessionPieces
fun getSessionTime(): Long = sessionTime
fun getSessionLevel(): Int = sessionLevel
// Getters for line clear stats (session)
fun getSessionSingles(): Int = sessionSingles
fun getSessionDoubles(): Int = sessionDoubles
fun getSessionTriples(): Int = sessionTriples
fun getSessionQuads(): Int = sessionQuads
// Getters for perfect clear stats
fun getSessionPerfectClears(): Int = sessionPerfectClears
fun getTotalPerfectClears(): Int = totalPerfectClears
fun resetStats() {
// Reset all lifetime stats
totalGames = 0
totalScore = 0
totalLines = 0
totalPieces = 0
totalTime = 0
maxLevel = 0
maxScore = 0
maxLines = 0
// Reset line clear stats
totalSingles = 0
totalDoubles = 0
totalTriples = 0
totalQuads = 0
// Reset perfect clear stats
totalPerfectClears = 0
// Save the reset stats
saveStats()
}
companion object {
private const val PREFS_NAME = "pixelmintdrop_stats"
private const val KEY_TOTAL_GAMES = "total_games"
private const val KEY_TOTAL_SCORE = "total_score"
private const val KEY_TOTAL_LINES = "total_lines"
private const val KEY_TOTAL_PIECES = "total_pieces"
private const val KEY_TOTAL_TIME = "total_time"
private const val KEY_MAX_LEVEL = "max_level"
private const val KEY_MAX_SCORE = "max_score"
private const val KEY_MAX_LINES = "max_lines"
private const val KEY_TOTAL_SINGLES = "total_singles"
private const val KEY_TOTAL_DOUBLES = "total_doubles"
private const val KEY_TOTAL_TRIPLES = "total_triples"
private const val KEY_TOTAL_QUADS = "total_quads"
private const val KEY_TOTAL_PERFECT_CLEARS = "total_perfect_clears"
} }
} }

View file

@ -0,0 +1,260 @@
package com.pixelmintdrop.model
/**
* Represents a game piece (Tetromino)
*/
enum class TetrominoType {
I, J, L, O, S, T, Z
}
class Tetromino(val type: TetrominoType) {
// Each tetromino has 4 rotations (0, 90, 180, 270 degrees)
private val blocks: Array<Array<BooleanArray>> = getBlocks(type)
private var currentRotation = 0
// Current position in the game grid
var x = 0
var y = 0
/**
* Get the current shape of the tetromino based on rotation
*/
fun getCurrentShape(): Array<BooleanArray> {
return blocks[currentRotation]
}
/**
* Get the width of the current tetromino shape
*/
fun getWidth(): Int {
return blocks[currentRotation][0].size
}
/**
* Get the height of the current tetromino shape
*/
fun getHeight(): Int {
return blocks[currentRotation].size
}
/**
* Rotate the tetromino clockwise
*/
fun rotateClockwise() {
currentRotation = (currentRotation + 1) % 4
}
/**
* Rotate the tetromino counter-clockwise
*/
fun rotateCounterClockwise() {
currentRotation = (currentRotation + 3) % 4
}
/**
* Check if the tetromino's block exists at the given coordinates
*/
fun isBlockAt(blockX: Int, blockY: Int): Boolean {
val shape = blocks[currentRotation]
return if (blockY >= 0 && blockY < shape.size &&
blockX >= 0 && blockX < shape[blockY].size) {
shape[blockY][blockX]
} else {
false
}
}
companion object {
/**
* Get the block patterns for each tetromino type and all its rotations
*/
private fun getBlocks(type: TetrominoType): Array<Array<BooleanArray>> {
return when (type) {
TetrominoType.I -> arrayOf(
// Rotation 0°
arrayOf(
booleanArrayOf(false, false, false, false),
booleanArrayOf(true, true, true, true),
booleanArrayOf(false, false, false, false),
booleanArrayOf(false, false, false, false)
),
// Rotation 90°
arrayOf(
booleanArrayOf(false, false, true, false),
booleanArrayOf(false, false, true, false),
booleanArrayOf(false, false, true, false),
booleanArrayOf(false, false, true, false)
),
// Rotation 180°
arrayOf(
booleanArrayOf(false, false, false, false),
booleanArrayOf(false, false, false, false),
booleanArrayOf(true, true, true, true),
booleanArrayOf(false, false, false, false)
),
// Rotation 270°
arrayOf(
booleanArrayOf(false, true, false, false),
booleanArrayOf(false, true, false, false),
booleanArrayOf(false, true, false, false),
booleanArrayOf(false, true, false, false)
)
)
TetrominoType.J -> arrayOf(
// Rotation 0°
arrayOf(
booleanArrayOf(true, false, false),
booleanArrayOf(true, true, true),
booleanArrayOf(false, false, false)
),
// Rotation 90°
arrayOf(
booleanArrayOf(false, true, true),
booleanArrayOf(false, true, false),
booleanArrayOf(false, true, false)
),
// Rotation 180°
arrayOf(
booleanArrayOf(false, false, false),
booleanArrayOf(true, true, true),
booleanArrayOf(false, false, true)
),
// Rotation 270°
arrayOf(
booleanArrayOf(false, true, false),
booleanArrayOf(false, true, false),
booleanArrayOf(true, true, false)
)
)
TetrominoType.L -> arrayOf(
// Rotation 0°
arrayOf(
booleanArrayOf(false, false, true),
booleanArrayOf(true, true, true),
booleanArrayOf(false, false, false)
),
// Rotation 90°
arrayOf(
booleanArrayOf(false, true, false),
booleanArrayOf(false, true, false),
booleanArrayOf(false, true, true)
),
// Rotation 180°
arrayOf(
booleanArrayOf(false, false, false),
booleanArrayOf(true, true, true),
booleanArrayOf(true, false, false)
),
// Rotation 270°
arrayOf(
booleanArrayOf(true, true, false),
booleanArrayOf(false, true, false),
booleanArrayOf(false, true, false)
)
)
TetrominoType.O -> arrayOf(
// All rotations are the same for O
arrayOf(
booleanArrayOf(false, true, true, false),
booleanArrayOf(false, true, true, false),
booleanArrayOf(false, false, false, false)
),
arrayOf(
booleanArrayOf(false, true, true, false),
booleanArrayOf(false, true, true, false),
booleanArrayOf(false, false, false, false)
),
arrayOf(
booleanArrayOf(false, true, true, false),
booleanArrayOf(false, true, true, false),
booleanArrayOf(false, false, false, false)
),
arrayOf(
booleanArrayOf(false, true, true, false),
booleanArrayOf(false, true, true, false),
booleanArrayOf(false, false, false, false)
)
)
TetrominoType.S -> arrayOf(
// Rotation 0°
arrayOf(
booleanArrayOf(false, true, true),
booleanArrayOf(true, true, false),
booleanArrayOf(false, false, false)
),
// Rotation 90°
arrayOf(
booleanArrayOf(false, true, false),
booleanArrayOf(false, true, true),
booleanArrayOf(false, false, true)
),
// Rotation 180°
arrayOf(
booleanArrayOf(false, false, false),
booleanArrayOf(false, true, true),
booleanArrayOf(true, true, false)
),
// Rotation 270°
arrayOf(
booleanArrayOf(true, false, false),
booleanArrayOf(true, true, false),
booleanArrayOf(false, true, false)
)
)
TetrominoType.T -> arrayOf(
// Rotation 0°
arrayOf(
booleanArrayOf(false, true, false),
booleanArrayOf(true, true, true),
booleanArrayOf(false, false, false)
),
// Rotation 90°
arrayOf(
booleanArrayOf(false, true, false),
booleanArrayOf(false, true, true),
booleanArrayOf(false, true, false)
),
// Rotation 180°
arrayOf(
booleanArrayOf(false, false, false),
booleanArrayOf(true, true, true),
booleanArrayOf(false, true, false)
),
// Rotation 270°
arrayOf(
booleanArrayOf(false, true, false),
booleanArrayOf(true, true, false),
booleanArrayOf(false, true, false)
)
)
TetrominoType.Z -> arrayOf(
// Rotation 0°
arrayOf(
booleanArrayOf(true, true, false),
booleanArrayOf(false, true, true),
booleanArrayOf(false, false, false)
),
// Rotation 90°
arrayOf(
booleanArrayOf(false, false, true),
booleanArrayOf(false, true, true),
booleanArrayOf(false, true, false)
),
// Rotation 180°
arrayOf(
booleanArrayOf(false, false, false),
booleanArrayOf(true, true, false),
booleanArrayOf(false, true, true)
),
// Rotation 270°
arrayOf(
booleanArrayOf(false, true, false),
booleanArrayOf(true, true, false),
booleanArrayOf(true, false, false)
)
)
}
}
}
}

View file

@ -0,0 +1,52 @@
package com.pixelmintdrop.theme
import android.graphics.Color
object ThemeManager {
// Theme colors
const val COLOR_CLASSIC_BACKGROUND = Color.BLACK
const val COLOR_CLASSIC_FOREGROUND = Color.WHITE
const val COLOR_CLASSIC_ACCENT = Color.CYAN
const val COLOR_NEON_BACKGROUND = 0xFF0D0221.toInt()
const val COLOR_NEON_FOREGROUND = 0xFFFF00FF.toInt()
const val COLOR_NEON_ACCENT = 0xFF00FFFF.toInt()
const val COLOR_MONOCHROME_BACKGROUND = 0xFF1A1A1A.toInt()
const val COLOR_MONOCHROME_FOREGROUND = Color.LTGRAY
const val COLOR_MONOCHROME_ACCENT = Color.WHITE
const val COLOR_RETRO_BACKGROUND = 0xFF3F2832.toInt()
const val COLOR_RETRO_FOREGROUND = 0xFFFF5A5F.toInt()
const val COLOR_RETRO_ACCENT = 0xFFFFB400.toInt()
const val COLOR_MINIMALIST_BACKGROUND = Color.WHITE
const val COLOR_MINIMALIST_FOREGROUND = Color.BLACK
const val COLOR_MINIMALIST_ACCENT = Color.DKGRAY
const val COLOR_GALAXY_BACKGROUND = 0xFF0B0C10.toInt()
const val COLOR_GALAXY_FOREGROUND = 0xFF66FCF1.toInt()
const val COLOR_GALAXY_ACCENT = 0xFF45A29E.toInt()
// Block colors for each piece type
const val COLOR_I_PIECE = 0xFF00F0F0.toInt()
const val COLOR_J_PIECE = 0xFF0000F0.toInt()
const val COLOR_L_PIECE = 0xFFF0A000.toInt()
const val COLOR_O_PIECE = 0xFFF0F000.toInt()
const val COLOR_S_PIECE = 0xFF00F000.toInt()
const val COLOR_T_PIECE = 0xFFA000F0.toInt()
const val COLOR_Z_PIECE = 0xFFF00000.toInt()
// Ghost piece colors
const val COLOR_GHOST_PIECE = 0x40FFFFFF
const val COLOR_GHOST_PIECE_GLOW = 0x20FFFFFF
// Grid colors
const val COLOR_GRID_LINE = 0x20FFFFFF
const val COLOR_GRID_BORDER = 0x40FFFFFF
// Effect colors
const val COLOR_LINE_CLEAR_FLASH = 0x80FFFFFF
const val COLOR_PERFECT_CLEAR_FLASH = 0xFFFFD700.toInt()
const val COLOR_COMBO_FLASH = 0x60FFFFFF
}