mirror of
https://github.com/cmclark00/mintris.git
synced 2025-05-17 21:15:21 +01:00
Update game piece handling and rename tetris to quad
This commit is contained in:
parent
e26c6ebd8c
commit
7babaeca50
10 changed files with 886 additions and 1738 deletions
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
currentPiece?.let { piece ->
|
||||
val ghostY = calculateGhostY(piece)
|
||||
val originalY = piece.y
|
||||
piece.y = ghostY
|
||||
|
||||
// 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
|
||||
ghostBlockPaint.alpha = 50
|
||||
drawPiece(canvas, piece, alpha = 50)
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// Save canvas state before drawing block effects
|
||||
canvas.save()
|
||||
private fun drawPiece(canvas: Canvas, piece: GamePiece, offsetX: Float = 0f, offsetY: Float = 0f, alpha: Int = 255) {
|
||||
val width = piece.getWidth()
|
||||
val height = piece.getHeight()
|
||||
|
||||
// Get the current block skin paint
|
||||
val paint = blockSkinPaints[currentBlockSkin] ?: blockSkinPaints["block_skin_1"]!!
|
||||
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
|
||||
|
||||
// Create a clone of the paint to avoid modifying the original
|
||||
val blockPaint = Paint(paint)
|
||||
// Draw block with subtle glow
|
||||
blockGlowPaint.alpha = alpha / 4
|
||||
canvas.drawRect(left, top, right, bottom, blockGlowPaint)
|
||||
|
||||
// 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 the main block
|
||||
canvas.drawRect(left + 1f, top + 1f, right - 1f, bottom - 1f, blockPaint)
|
||||
|
||||
// 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)
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
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 + 2),
|
||||
canvas.height.toFloat() / (height + 2)
|
||||
)
|
||||
// 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 - width * previewBlockSize) / 2
|
||||
val previewTop = (canvas.height - height * previewBlockSize) / 2
|
||||
// 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 * previewBlockSize + previewBlockSize,
|
||||
previewTop + height * previewBlockSize + previewBlockSize,
|
||||
glowPaint
|
||||
)
|
||||
// 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 * previewBlockSize
|
||||
val top = previewTop + y * previewBlockSize
|
||||
val right = left + previewBlockSize
|
||||
val bottom = top + previewBlockSize
|
||||
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 + 1, top + 1, right - 1, bottom - 1)
|
||||
canvas.drawRect(rect, blockPaint)
|
||||
// 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)
|
||||
}
|
||||
// Draw subtle border glow
|
||||
val glowRect = RectF(left, top, right, bottom)
|
||||
canvas.drawRect(glowRect, glowPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
val width: Int = WIDTH
|
||||
val height: Int = HEIGHT
|
||||
|
||||
// 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
|
||||
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
|
||||
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!!
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next piece that will be spawned
|
||||
*/
|
||||
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")
|
||||
fun getCurrentPiece(): GamePiece? = currentPiece
|
||||
|
||||
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 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()
|
||||
}
|
||||
}
|
||||
fun wouldCollide(piece: GamePiece, newX: Int, newY: Int): Boolean {
|
||||
val width = piece.getWidth()
|
||||
val height = piece.getHeight()
|
||||
|
||||
/**
|
||||
* 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)) {
|
||||
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
|
||||
// Increase speed based on lines cleared
|
||||
if (linesCleared > 0) {
|
||||
dropInterval = (dropInterval * 0.95).toLong().coerceAtLeast(MIN_DROP_INTERVAL)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
private fun isLineFull(y: Int): Boolean {
|
||||
for (x in 0 until WIDTH) {
|
||||
if (!board[y][x]) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
private fun createRandomPiece(): GamePiece {
|
||||
return GamePiece(GamePieceType.values().random())
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the game level
|
||||
*/
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
48
app/src/main/java/com/pixelmintdrop/model/GamePieceType.kt
Normal file
48
app/src/main/java/com/pixelmintdrop/model/GamePieceType.kt
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
private var sessionScore = 0
|
||||
private var sessionLines = 0
|
||||
private var sessionQuads = 0
|
||||
private var sessionPerfectClears = 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
|
||||
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)
|
||||
|
||||
// Perfect clear stats
|
||||
private var sessionPerfectClears: Int = 0
|
||||
private var totalPerfectClears: Int = 0
|
||||
fun getSessionScore(): Int = sessionScore
|
||||
fun getSessionLines(): Int = sessionLines
|
||||
fun getSessionQuads(): Int = sessionQuads
|
||||
fun getSessionPerfectClears(): Int = sessionPerfectClears
|
||||
|
||||
init {
|
||||
loadStats()
|
||||
}
|
||||
fun updateStats(score: Int, lines: Int, isQuad: Boolean, isPerfectClear: Boolean) {
|
||||
// Update session stats
|
||||
sessionScore += score
|
||||
sessionLines += lines
|
||||
if (isQuad) sessionQuads++
|
||||
if (isPerfectClear) sessionPerfectClears++
|
||||
|
||||
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)
|
||||
// Update high score if needed
|
||||
if (sessionScore > getHighScore()) {
|
||||
prefs.edit().putInt(KEY_HIGH_SCORE, sessionScore).apply()
|
||||
}
|
||||
|
||||
// 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() {
|
||||
// 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()
|
||||
}
|
||||
}
|
260
app/src/main/java/com/pixelmintdrop/model/Tetromino.kt
Normal file
260
app/src/main/java/com/pixelmintdrop/model/Tetromino.kt
Normal 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)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
52
app/src/main/java/com/pixelmintdrop/theme/ThemeManager.kt
Normal file
52
app/src/main/java/com/pixelmintdrop/theme/ThemeManager.kt
Normal 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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue