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 com.pixelmintdrop.R
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
@ -111,13 +111,13 @@ class GameAccessibilityHelper(private val context: Context) {
val pieceType = gameView.getCurrentPieceType() ?: return "No piece"
return when (pieceType) {
TetrominoType.I -> "I piece, long bar"
TetrominoType.J -> "J piece, hook shape pointing left"
TetrominoType.L -> "L piece, hook shape pointing right"
TetrominoType.O -> "O piece, square shape"
TetrominoType.S -> "S piece, zigzag shape"
TetrominoType.T -> "T piece, T shape"
TetrominoType.Z -> "Z piece, reverse zigzag shape"
GamePieceType.I -> "I piece, long bar"
GamePieceType.J -> "J piece, hook shape pointing left"
GamePieceType.L -> "L piece, hook shape pointing right"
GamePieceType.O -> "O piece, square shape"
GamePieceType.S -> "S piece, zigzag shape"
GamePieceType.T -> "T piece, T shape"
GamePieceType.Z -> "Z piece, reverse zigzag shape"
}
}
@ -134,4 +134,19 @@ class GameAccessibilityHelper(private val context: Context) {
fun announceLevelUp(view: View, newLevel: Int) {
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.ThemeType
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
/**
@ -112,9 +50,13 @@ class GameView @JvmOverloads constructor(
// Game state
private var isRunning = false
var isPaused = false // Changed from private to public to allow access from MainActivity
var isPaused = false
private var score = 0
// Current piece
private var currentPiece: GamePiece? = null
private var nextPiece: GamePiece? = null
// Callbacks
var onNextPieceChanged: (() -> Unit)? = null
@ -135,22 +77,22 @@ class GameView @JvmOverloads constructor(
private val ghostBlockPaint = Paint().apply {
color = Color.WHITE
alpha = 80 // 30% opacity
alpha = 80
isAntiAlias = true
}
private val gridPaint = Paint().apply {
color = Color.parseColor("#222222") // Very dark gray
alpha = 20 // Reduced from 40 to be more subtle
color = Color.parseColor("#222222")
alpha = 20
isAntiAlias = true
strokeWidth = 1f
style = Paint.Style.STROKE
maskFilter = null // Ensure no blur effect on grid lines
maskFilter = null
}
private val glowPaint = Paint().apply {
color = Color.WHITE
alpha = 40 // Reduced from 80 for more subtlety
alpha = 40
isAntiAlias = true
style = Paint.Style.STROKE
strokeWidth = 1.5f
@ -165,24 +107,20 @@ class GameView @JvmOverloads constructor(
maskFilter = BlurMaskFilter(12f, BlurMaskFilter.Blur.OUTER)
}
// Add a new paint for the pulse effect
private val pulsePaint = Paint().apply {
color = Color.CYAN
alpha = 255
isAntiAlias = true
style = Paint.Style.FILL
maskFilter = BlurMaskFilter(32f, BlurMaskFilter.Blur.OUTER) // Increased from 16f to 32f
maskFilter = BlurMaskFilter(32f, BlurMaskFilter.Blur.OUTER)
}
// Pre-allocate paint objects to avoid GC
private val tmpPaint = Paint()
// Calculate block size based on view dimensions and board size
private var blockSize = 0f
private var boardLeft = 0f
private var boardTop = 0f
// Game loop handler and runnable
private val handler = Handler(Looper.getMainLooper())
private val gameLoopRunnable = object : Runnable {
override fun run() {
@ -194,7 +132,6 @@ class GameView @JvmOverloads constructor(
}
}
// Touch parameters
private var lastTouchX = 0f
private var lastTouchY = 0f
private var startX = 0f
@ -495,197 +432,29 @@ class GameView @JvmOverloads constructor(
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
// Force hardware acceleration - Critical for performance
setLayerType(LAYER_TYPE_HARDWARE, null)
// Calculate block size based on view dimensions and board size
blockSize = minOf(
w / (gameBoard.width + 2f), // Add padding
h / (gameBoard.height + 2f) // Add padding
)
// Update gesture exclusion rect for edge-to-edge rendering
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
setSystemGestureExclusionRects(listOf(Rect(0, 0, w, h)))
}
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")
// Center the board
boardLeft = (w - gameBoard.width * blockSize) / 2f
boardTop = (h - gameBoard.height * blockSize) / 2f
}
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)
// Draw background (already black from theme)
// Draw the game board
drawBoard(canvas)
// Draw board border glow
drawBoardBorder(canvas)
// Draw ghost piece
drawGhostPiece(canvas)
// Draw grid (very subtle)
drawGrid(canvas)
// 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()
}
// Draw current piece
currentPiece?.let { piece ->
drawPiece(canvas, piece)
}
}
@ -843,219 +612,52 @@ class GameView @JvmOverloads constructor(
* Draw the ghost piece (landing preview)
*/
private fun drawGhostPiece(canvas: Canvas) {
val piece = gameBoard.getCurrentPiece() ?: return
val ghostY = gameBoard.getGhostY()
// Draw semi-transparent background for each block
for (y in 0 until piece.getHeight()) {
for (x in 0 until piece.getWidth()) {
if (piece.isBlockAt(x, y)) {
val boardX = piece.x + x
val boardY = ghostY + y
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
)
}
}
}
currentPiece?.let { piece ->
val ghostY = calculateGhostY(piece)
val originalY = piece.y
piece.y = ghostY
ghostBlockPaint.alpha = 50
drawPiece(canvas, piece, alpha = 50)
piece.y = originalY
}
}
/**
* Draw a single tetris block at the given grid position
*/
private fun drawBlock(canvas: Canvas, x: Int, y: Int, isGhost: Boolean, isPulsingLine: Boolean) {
val left = boardLeft + x * blockSize
val top = boardTop + y * blockSize
val right = left + blockSize
val bottom = top + blockSize
private fun calculateGhostY(piece: GamePiece): Int {
var testY = piece.y
while (testY < gameBoard.height && !gameBoard.wouldCollide(piece, piece.x, testY + 1)) {
testY++
}
return testY
}
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
canvas.save()
// Get the current block skin paint
val paint = blockSkinPaints[currentBlockSkin] ?: blockSkinPaints["block_skin_1"]!!
// Create a clone of the paint to avoid modifying the original
val blockPaint = Paint(paint)
// Draw block based on current skin
when (currentBlockSkin) {
"block_skin_1" -> { // Classic
// Draw outer glow
blockGlowPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE
canvas.drawRect(left - 2f, top - 2f, right + 2f, bottom + 2f, blockGlowPaint)
// Draw block
blockPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE
blockPaint.alpha = if (isGhost) 30 else 255
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)
blockPaint.alpha = alpha
for (y in 0 until height) {
for (x in 0 until width) {
if (piece.isBlockAt(x, y)) {
val left = offsetX + (piece.x + x) * blockSize
val top = offsetY + (piece.y + y) * blockSize
val right = left + blockSize
val bottom = top + blockSize
// Draw block with subtle glow
blockGlowPaint.alpha = alpha / 4
canvas.drawRect(left, top, right, bottom, blockGlowPaint)
// Draw the main block
canvas.drawRect(left + 1f, top + 1f, right - 1f, bottom - 1f, blockPaint)
// Draw border glow
glowPaint.alpha = alpha / 2
canvas.drawRect(left, top, right, bottom, glowPaint)
}
}
}
// Draw pulse effect if animation is active and this is a pulsing line
if (isPulsing && isPulsingLine) {
val pulseBlockPaint = Paint().apply {
color = Color.WHITE
alpha = (255 * pulseAlpha).toInt()
isAntiAlias = true
style = Paint.Style.FILL
maskFilter = BlurMaskFilter(40f * (1f + pulseAlpha), BlurMaskFilter.Blur.OUTER)
}
canvas.drawRect(left - 16f, top - 16f, right + 16f, bottom + 16f, pulseBlockPaint)
}
// Restore canvas state after drawing block effects
canvas.restore()
}
/**
@ -1093,16 +695,7 @@ class GameView @JvmOverloads constructor(
// Custom touch event handling
override fun onTouchEvent(event: MotionEvent): Boolean {
if (!isRunning || isPaused || gameBoard.isGameOver) {
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
}
if (!isRunning || isPaused) return false
when (event.action) {
MotionEvent.ACTION_DOWN -> {
@ -1110,120 +703,53 @@ class GameView @JvmOverloads constructor(
startY = event.y
lastTouchX = event.x
lastTouchY = event.y
lastTapTime = currentTime // Set the tap time when touch starts
// Reset direction lock
lockedDirection = null
lastTapTime = System.currentTimeMillis()
return true
}
MotionEvent.ACTION_MOVE -> {
val deltaX = event.x - lastTouchX
val deltaY = event.y - lastTouchY
val dx = event.x - lastTouchX
val dy = event.y - lastTouchY
// Check if we should lock direction
if (lockedDirection == null) {
val absDeltaX = abs(deltaX)
val absDeltaY = abs(deltaY)
if (absDeltaX > blockSize * directionLockThreshold ||
absDeltaY > blockSize * directionLockThreshold) {
// Lock to the dominant direction
lockedDirection = if (absDeltaX > absDeltaY) {
Direction.HORIZONTAL
} else {
Direction.VERTICAL
}
// Horizontal movement
if (abs(dx) > blockSize / 2) {
if (dx > 0) {
movePieceRight()
} else {
movePieceLeft()
}
lastTouchX = event.x
}
// Handle movement based on locked direction
when (lockedDirection) {
Direction.HORIZONTAL -> {
if (abs(deltaX) > blockSize * minMovementThreshold) {
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
}
// Vertical movement (soft drop)
if (dy > blockSize / 2) {
dropPiece()
lastTouchY = event.y
}
return true
}
MotionEvent.ACTION_UP -> {
val deltaX = event.x - startX
val deltaY = event.y - startY
val moveTime = currentTime - lastTapTime
val dx = event.x - startX
val dy = event.y - startY
val tapTime = System.currentTimeMillis() - lastTapTime
// Handle taps for rotation
if (moveTime < minTapTime * 1.5 &&
abs(deltaY) < maxTapMovement * 1.5 &&
abs(deltaX) < maxTapMovement * 1.5) {
if (currentTime - lastRotationTime >= rotationCooldown) {
gameBoard.rotate()
lastRotationTime = currentTime
gameHaptics?.vibrateForPieceMove()
invalidate()
}
return true
// Detect tap for rotation
if (abs(dx) < blockSize / 2 && abs(dy) < blockSize / 2 && tapTime < 200) {
rotatePiece()
}
// Handle gestures
// Check for hold gesture (swipe up)
if (deltaY < -blockSize * minHoldDistance &&
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()
// Detect swipe down for hard drop
if (dy > height / 4) {
hardDrop()
}
// Reset direction lock
lockedDirection = null
return true
}
}
return true
return false
}
/**
@ -1249,7 +775,7 @@ class GameView @JvmOverloads constructor(
/**
* Get the next piece that will be spawned
*/
fun getNextPiece(): Tetromino? {
fun getNextPiece(): GamePiece? {
return gameBoard.getNextPiece()
}
@ -1364,7 +890,7 @@ class GameView @JvmOverloads constructor(
// Create new animation
pulseAnimator = ValueAnimator.ofFloat(0f, 1f, 0f).apply {
duration = when (lineCount) {
4 -> 2000L // Quad - longer duration
4 -> 2000L // Tetris - longer duration
3 -> 1600L // Triples
2 -> 1200L // Doubles
1 -> 1000L // Singles
@ -1609,4 +1135,103 @@ class GameView @JvmOverloads constructor(
gameHaptics?.vibrateForPieceMove()
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)
// Get the next piece from game view
gameView?.let {
it.getNextPiece()?.let { piece ->
val width = piece.getWidth()
val height = piece.getHeight()
// Calculate block size for the preview (smaller than main board)
val previewBlockSize = min(
canvas.width.toFloat() / (width + 2),
canvas.height.toFloat() / (height + 2)
)
// Center the piece in the preview area
val previewLeft = (canvas.width - width * previewBlockSize) / 2
val previewTop = (canvas.height - height * previewBlockSize) / 2
// Draw subtle background glow
val glowPaint = Paint().apply {
color = Color.WHITE
alpha = 10
maskFilter = BlurMaskFilter(previewBlockSize * 0.5f, BlurMaskFilter.Blur.OUTER)
}
canvas.drawRect(
previewLeft - previewBlockSize,
previewTop - previewBlockSize,
previewLeft + width * previewBlockSize + previewBlockSize,
previewTop + height * previewBlockSize + previewBlockSize,
glowPaint
)
for (y in 0 until height) {
for (x in 0 until width) {
if (piece.isBlockAt(x, y)) {
val left = previewLeft + x * previewBlockSize
val top = previewTop + y * previewBlockSize
val right = left + previewBlockSize
val bottom = top + previewBlockSize
// Draw block with subtle glow
val rect = RectF(left + 1, top + 1, right - 1, bottom - 1)
canvas.drawRect(rect, blockPaint)
// Draw subtle border glow
val glowRect = RectF(left, top, right, bottom)
canvas.drawRect(glowRect, glowPaint)
}
gameView?.getNextPiece()?.let { piece ->
val width = piece.getWidth()
val height = piece.getHeight()
// Calculate block size for the preview (smaller than main board)
val previewBlockSize = min(
canvas.width.toFloat() / (width.toFloat() + 2f),
canvas.height.toFloat() / (height.toFloat() + 2f)
)
// Center the piece in the preview area
val previewLeft = (canvas.width.toFloat() - width.toFloat() * previewBlockSize) / 2f
val previewTop = (canvas.height.toFloat() - height.toFloat() * previewBlockSize) / 2f
// Draw subtle background glow
val glowPaint = Paint().apply {
color = Color.WHITE
alpha = 10
maskFilter = BlurMaskFilter(previewBlockSize * 0.5f, BlurMaskFilter.Blur.OUTER)
}
canvas.drawRect(
previewLeft - previewBlockSize,
previewTop - previewBlockSize,
previewLeft + width.toFloat() * previewBlockSize + previewBlockSize,
previewTop + height.toFloat() * previewBlockSize + previewBlockSize,
glowPaint
)
for (y in 0 until height) {
for (x in 0 until width) {
if (piece.isBlockAt(x, y)) {
val left = previewLeft + x.toFloat() * previewBlockSize
val top = previewTop + y.toFloat() * previewBlockSize
val right = left + previewBlockSize
val bottom = top + previewBlockSize
// Draw block with subtle glow
val rect = RectF(left + 1f, top + 1f, right - 1f, bottom - 1f)
canvas.drawRect(rect, blockPaint)
// Draw subtle border glow
val glowRect = RectF(left, top, right, bottom)
canvas.drawRect(glowRect, glowPaint)
}
}
}

View file

@ -3,763 +3,197 @@ package com.pixelmintdrop.model
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(
val width: Int = 10,
val height: Int = 20
) {
class GameBoard {
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
// True = occupied, False = empty
private val grid = Array(height) { BooleanArray(width) { false } }
// Current active piece
// Board state
private val board = Array(HEIGHT) { Array(WIDTH) { false } }
private var currentPiece: GamePiece? = null
// Next piece to be played
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
var score = 0
var level = 1
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
var dropInterval = INITIAL_DROP_INTERVAL
private set
// Scoring state
private var combo = 0
private var lastClearWasQuad = false
private var lastClearWasPerfect = false
private var lastClearWasAllClear = false
private var lastPieceClearedLines = false // Track if the last piece placed cleared lines
// Animation state
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
val width: Int = WIDTH
val height: Int = HEIGHT
fun reset() {
// Clear the board
for (y in 0 until HEIGHT) {
for (x in 0 until WIDTH) {
board[y][x] = false
}
}
canHold = false
// Reset pieces
currentPiece = null
nextPiece = null
// Reset drop interval
dropInterval = INITIAL_DROP_INTERVAL
}
/**
* Get the currently held piece
*/
fun getHoldPiece(): GamePiece? = holdPiece
/**
* Get the next piece that will be spawned
*/
fun spawnNewPiece(): GamePiece {
// Get the next piece or create a new one if none exists
currentPiece = nextPiece ?: createRandomPiece()
nextPiece = createRandomPiece()
// Position the piece at the top center of the board
currentPiece?.let { piece ->
piece.x = (WIDTH - piece.getWidth()) / 2
piece.y = 0
}
return currentPiece!!
}
fun getNextPiece(): GamePiece? = nextPiece
/**
* Spawns the current tetromino at the top of the board
*/
fun spawnPiece() {
Log.d(TAG, "spawnPiece() started - current states: isHardDropInProgress=$isHardDropInProgress, isPieceLocking=$isPieceLocking")
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 getCurrentPiece(): GamePiece? = currentPiece
fun hasBlockAt(x: Int, y: Int): Boolean {
if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) return false
return board[y][x]
}
/**
* Move the current piece left
*/
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
fun wouldCollide(piece: GamePiece, newX: Int, newY: Int): Boolean {
val width = piece.getWidth()
val height = piece.getHeight()
return if (canMove(0, 1)) {
currentPiece?.y = currentPiece?.y?.plus(1) ?: 0
// 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()) {
for (y in 0 until height) {
for (x in 0 until width) {
if (piece.isBlockAt(x, y)) {
val boardX = newX + x
val boardY = newY + y
// Check if the position is outside the board horizontally
if (boardX < 0 || boardX >= width) {
return false
// Check board boundaries
if (boardX < 0 || boardX >= WIDTH || boardY >= HEIGHT) {
return true
}
// Check if the position is below the board
if (boardY >= height) {
return false
}
// 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
// Check collision with placed blocks
if (boardY >= 0 && board[boardY][boardX]) {
return true
}
}
}
}
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
}
/**
* Lock the current piece in place
*/
private fun lockPiece() {
if (isPieceLocking) {
Log.d(TAG, "lockPiece() called but blocked: isPieceLocking=$isPieceLocking")
return // Prevent recursive locking
}
fun lockPiece(piece: GamePiece) {
val width = piece.getWidth()
val height = piece.getHeight()
Log.d(TAG, "lockPiece() started - setting isPieceLocking=true, current isHardDropInProgress=$isHardDropInProgress")
isPieceLocking = true
val piece = currentPiece ?: return
// Add the piece to the grid
for (y in 0 until piece.getHeight()) {
for (x in 0 until piece.getWidth()) {
// Place the piece on the board
for (y in 0 until height) {
for (x in 0 until width) {
if (piece.isBlockAt(x, y)) {
val boardX = piece.x + x
val boardY = piece.y + y
// Only add to grid if within bounds
if (boardY >= 0 && boardY < height && boardX >= 0 && boardX < width) {
grid[boardY][boardX] = true
if (boardY >= 0 && boardY < HEIGHT && boardX >= 0 && boardX < WIDTH) {
board[boardY][boardX] = true
}
}
}
}
// Trigger the piece lock vibration
onPieceLock?.invoke()
// 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")
// Clear any completed lines
clearLines()
}
/**
* Find and clear completed lines immediately
*/
private fun findAndClearLines() {
// Quick scan for completed lines
var shiftAmount = 0
var y = height - 1
val linesToClear = mutableListOf<Int>()
private fun clearLines() {
var linesCleared = 0
var y = HEIGHT - 1
while (y >= 0) {
if (grid[y].all { it }) {
// Line is full, add to lines to clear
linesToClear.add(y)
shiftAmount++
} else if (shiftAmount > 0) {
// 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)
if (isLineFull(y)) {
// Move all lines above down
for (moveY in y downTo 1) {
for (x in 0 until WIDTH) {
board[moveY][x] = board[moveY - 1][x]
}
}
}
// Clear top rows after callback
for (y in 0 until shiftAmount) {
java.util.Arrays.fill(grid[y], false)
}
Thread {
calculateScore(shiftAmount)
}.start()
}
// Update combo based on whether this piece cleared lines
if (shiftAmount > 0) {
if (lastPieceClearedLines) {
combo++
// Clear top line
for (x in 0 until WIDTH) {
board[0][x] = false
}
linesCleared++
} else {
combo = 1 // Start new combo
}
} 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
y--
}
}
// Ensure ghostY doesn't exceed the board height
return ghostY.coerceAtMost(height - 1)
}
/**
* 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()
// Increase speed based on lines cleared
if (linesCleared > 0) {
dropInterval = (dropInterval * 0.95).toLong().coerceAtLeast(MIN_DROP_INTERVAL)
}
}
/**
* Get the drop interval for the given level
*/
private fun getDropIntervalForLevel(level: Int): Long {
val cappedLevel = level.coerceIn(1, 20)
// Update game speed based on level (NES formula)
return (1000 * Math.pow(0.8, (cappedLevel - 1).toDouble())).toLong()
private fun isLineFull(y: Int): Boolean {
for (x in 0 until WIDTH) {
if (!board[y][x]) return false
}
return true
}
/**
* Update the game level
*/
private fun createRandomPiece(): GamePiece {
return GamePiece(GamePieceType.values().random())
}
}

View file

@ -1,214 +1,48 @@
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) {
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<BooleanArray>> = getBlocks(type)
private val blocks: Array<Array<Boolean>> = type.getBlocks()
/**
* Get the current shape of the piece based on rotation
*/
fun getShape(): Array<BooleanArray> = blocks[rotation]
fun getWidth(): Int = blocks[0].size
/**
* Get the width of the current piece shape
*/
fun getWidth(): Int = blocks[rotation][0].size
fun getHeight(): Int = blocks.size
/**
* 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 {
fun isBlockAt(x: Int, y: Int): Boolean {
if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight()) return false
return blocks[rotation][y][x]
return blocks[y][x]
}
/**
* Get the block patterns for each piece type and all its rotations
*/
private fun getBlocks(type: GamePieceType): Array<Array<BooleanArray>> {
return when (type) {
GamePieceType.I -> arrayOf(
arrayOf(
booleanArrayOf(false, false, false, false),
booleanArrayOf(true, true, true, true),
booleanArrayOf(false, false, false, false),
booleanArrayOf(false, false, false, false)
),
arrayOf(
booleanArrayOf(false, false, true, false),
booleanArrayOf(false, false, true, false),
booleanArrayOf(false, false, true, false),
booleanArrayOf(false, false, true, false)
),
arrayOf(
booleanArrayOf(false, false, false, false),
booleanArrayOf(false, false, false, false),
booleanArrayOf(true, true, true, true),
booleanArrayOf(false, false, false, false)
),
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)
)
)
fun rotate() {
rotation = (rotation + 1) % 4
rotateBlocks()
}
private fun rotateBlocks() {
val width = getWidth()
val height = getHeight()
val rotated = Array(width) { Array(height) { false } }
for (y in 0 until height) {
for (x in 0 until width) {
when (rotation) {
1 -> rotated[x][height - 1 - y] = blocks[y][x]
2 -> rotated[height - 1 - y][width - 1 - x] = blocks[y][x]
3 -> rotated[width - 1 - x][y] = blocks[y][x]
else -> rotated[y][x] = blocks[y][x]
}
}
}
blocks.indices.forEach { i ->
blocks[i] = rotated[i].copyOf()
}
}
}

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.
*/
fun calculateGameXP(score: Int, lines: Int, level: Int, timePlayedMs: Long,
quadCount: Int, perfectClearCount: Int): Long {
val scoreXP = (score * SCORE_XP_MULTIPLIER).toLong()
val linesXP = (lines * LINES_XP_MULTIPLIER).toLong()
val quadBonus = (quadCount * QUAD_XP_BONUS).toLong()
val perfectClearBonus = (perfectClearCount * 100).toLong()
val timeBonus = (timePlayedMs * TIME_XP_MULTIPLIER).toLong()
fun calculateGameXP(
score: Int,
lines: Int,
level: Int,
timePlayedMs: Long,
quadCount: Int,
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
)
private const val SCORE_XP_MULTIPLIER = 0.1
private const val LINES_XP_MULTIPLIER = 5
private const val QUAD_XP_BONUS = 50
private const val TIME_XP_MULTIPLIER = 0.01
private const val BASE_SCORE_XP = 0.1 // XP per score point
private const val BASE_LINES_XP = 100.0 // XP per line cleared
private const val BASE_QUAD_BONUS = 500.0 // Bonus XP for clearing 4 lines at once
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.SharedPreferences
/**
* Manages game statistics and high scores
*/
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)
// 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
private var sessionScore: Int = 0
private var sessionLines: Int = 0
private var sessionPieces: Int = 0
private var sessionTime: Long = 0
private var sessionLevel: Int = 0
// Line clear stats (session)
private var sessionSingles: Int = 0
private var sessionDoubles: Int = 0
private var sessionTriples: Int = 0
private var sessionQuads: Int = 0
// Perfect clear stats
private var sessionPerfectClears: Int = 0
private var totalPerfectClears: Int = 0
init {
loadStats()
}
private fun loadStats() {
totalGames = prefs.getInt(KEY_TOTAL_GAMES, 0)
totalScore = prefs.getLong(KEY_TOTAL_SCORE, 0)
totalLines = prefs.getInt(KEY_TOTAL_LINES, 0)
totalPieces = prefs.getInt(KEY_TOTAL_PIECES, 0)
totalTime = prefs.getLong(KEY_TOTAL_TIME, 0)
maxLevel = prefs.getInt(KEY_MAX_LEVEL, 0)
maxScore = prefs.getInt(KEY_MAX_SCORE, 0)
maxLines = prefs.getInt(KEY_MAX_LINES, 0)
// 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() {
private var sessionScore = 0
private var sessionLines = 0
private var sessionQuads = 0
private var sessionPerfectClears = 0
fun getHighScore(): Int = prefs.getInt(KEY_HIGH_SCORE, 0)
fun getTotalGames(): Int = prefs.getInt(KEY_TOTAL_GAMES, 0)
fun getTotalLines(): Int = prefs.getInt(KEY_TOTAL_LINES, 0)
fun getTotalQuads(): Int = prefs.getInt(KEY_TOTAL_QUADS, 0)
fun getTotalPerfectClears(): Int = prefs.getInt(KEY_TOTAL_PERFECT_CLEARS, 0)
fun getSessionScore(): Int = sessionScore
fun getSessionLines(): Int = sessionLines
fun getSessionQuads(): Int = sessionQuads
fun getSessionPerfectClears(): Int = sessionPerfectClears
fun updateStats(score: Int, lines: Int, isQuad: Boolean, isPerfectClear: Boolean) {
// Update session stats
sessionScore += score
sessionLines += lines
if (isQuad) sessionQuads++
if (isPerfectClear) sessionPerfectClears++
// Update high score if needed
if (sessionScore > getHighScore()) {
prefs.edit().putInt(KEY_HIGH_SCORE, sessionScore).apply()
}
// Update total stats
prefs.edit()
.putInt(KEY_TOTAL_GAMES, totalGames)
.putLong(KEY_TOTAL_SCORE, totalScore)
.putInt(KEY_TOTAL_LINES, totalLines)
.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)
.putInt(KEY_TOTAL_LINES, getTotalLines() + lines)
.putInt(KEY_TOTAL_QUADS, getTotalQuads() + if (isQuad) 1 else 0)
.putInt(KEY_TOTAL_PERFECT_CLEARS, getTotalPerfectClears() + if (isPerfectClear) 1 else 0)
.apply()
}
fun startNewSession() {
fun startNewGame() {
// Increment total games counter
prefs.edit().putInt(KEY_TOTAL_GAMES, getTotalGames() + 1).apply()
}
fun resetSession() {
sessionScore = 0
sessionLines = 0
sessionPieces = 0
sessionTime = 0
sessionLevel = 0
sessionSingles = 0
sessionDoubles = 0
sessionTriples = 0
sessionQuads = 0
sessionPerfectClears = 0
}
fun updateSessionStats(score: Int, lines: Int, pieces: Int, time: Long, level: Int) {
sessionScore = score
sessionLines = lines
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"
fun resetAllStats() {
prefs.edit().clear().apply()
resetSession()
}
}

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
}