mirror of
https://github.com/cmclark00/mintris.git
synced 2025-05-18 20:05:20 +01:00
Fix namespace and applicationId in build.gradle to match package structure
This commit is contained in:
parent
f5f135ff27
commit
38163c33a3
39 changed files with 134 additions and 157 deletions
765
app/src/main/java/com/pixelmintdrop/model/GameBoard.kt
Normal file
765
app/src/main/java/com/pixelmintdrop/model/GameBoard.kt
Normal file
|
@ -0,0 +1,765 @@
|
|||
package com.pixelmintdrop.model
|
||||
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* Represents the game board (grid) and manages game state
|
||||
*/
|
||||
class GameBoard(
|
||||
val width: Int = 10,
|
||||
val height: Int = 20
|
||||
) {
|
||||
companion object {
|
||||
private const val TAG = "GameBoard"
|
||||
}
|
||||
|
||||
// Board grid to track locked pieces
|
||||
// True = occupied, False = empty
|
||||
private val grid = Array(height) { BooleanArray(width) { false } }
|
||||
|
||||
// Current active tetromino
|
||||
private var currentPiece: Tetromino? = null
|
||||
|
||||
// Next tetromino to be played
|
||||
private var nextPiece: Tetromino? = null
|
||||
|
||||
// Hold piece
|
||||
private var holdPiece: Tetromino? = null
|
||||
private var canHold = true
|
||||
|
||||
// 7-bag randomizer
|
||||
private val bag = mutableListOf<TetrominoType>()
|
||||
|
||||
// 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
|
||||
|
||||
// Scoring state
|
||||
private var combo = 0
|
||||
private var lastClearWasTetris = false
|
||||
private var lastClearWasPerfect = false
|
||||
private var lastClearWasAllClear = false
|
||||
private var lastPieceClearedLines = false // Track if the last piece placed cleared lines
|
||||
|
||||
// Animation state
|
||||
var linesToClear = mutableListOf<Int>()
|
||||
var isLineClearAnimationInProgress = false
|
||||
|
||||
// Initial game speed (milliseconds per drop)
|
||||
var dropInterval = 1000L
|
||||
|
||||
// Callbacks for game events
|
||||
var onPieceMove: (() -> Unit)? = null
|
||||
var onPieceLock: (() -> Unit)? = null
|
||||
var onNextPieceChanged: (() -> Unit)? = null
|
||||
var onLineClear: ((Int, List<Int>) -> Unit)? = null
|
||||
var onPiecePlaced: (() -> Unit)? = null // New callback for when a piece is placed
|
||||
|
||||
// Store the last cleared lines
|
||||
private val lastClearedLines = mutableListOf<Int>()
|
||||
|
||||
// Add spawn protection variables
|
||||
private var pieceSpawnTime = 0L
|
||||
private val spawnGracePeriod = 250L // Changed from 150ms to 250ms
|
||||
|
||||
init {
|
||||
spawnNextPiece()
|
||||
spawnPiece()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the next tetromino piece using 7-bag randomizer
|
||||
*/
|
||||
private fun spawnNextPiece() {
|
||||
// If bag is empty, refill it with all piece types
|
||||
if (bag.isEmpty()) {
|
||||
bag.addAll(TetrominoType.entries.toTypedArray())
|
||||
bag.shuffle()
|
||||
}
|
||||
|
||||
// Take the next piece from the bag
|
||||
nextPiece = Tetromino(bag.removeAt(0))
|
||||
onNextPieceChanged?.invoke()
|
||||
}
|
||||
|
||||
/**
|
||||
* Hold the current piece
|
||||
*/
|
||||
fun holdPiece() {
|
||||
if (!canHold) return
|
||||
|
||||
val current = currentPiece
|
||||
if (holdPiece == null) {
|
||||
// If no piece is held, hold current piece and spawn new one
|
||||
holdPiece = current
|
||||
currentPiece = nextPiece
|
||||
spawnNextPiece()
|
||||
// Reset position of new piece
|
||||
currentPiece?.apply {
|
||||
x = (width - getWidth()) / 2
|
||||
y = 0
|
||||
}
|
||||
} else {
|
||||
// Swap current piece with held piece
|
||||
currentPiece = holdPiece
|
||||
holdPiece = current
|
||||
// Reset position of swapped piece
|
||||
currentPiece?.apply {
|
||||
x = (width - getWidth()) / 2
|
||||
y = 0
|
||||
}
|
||||
}
|
||||
canHold = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently held piece
|
||||
*/
|
||||
fun getHoldPiece(): Tetromino? = holdPiece
|
||||
|
||||
/**
|
||||
* Get the next piece that will be spawned
|
||||
*/
|
||||
fun getNextPiece(): Tetromino? = nextPiece
|
||||
|
||||
/**
|
||||
* Spawns the current tetromino at the top of the board
|
||||
*/
|
||||
fun spawnPiece() {
|
||||
Log.d(TAG, "spawnPiece() started - current states: isHardDropInProgress=$isHardDropInProgress, isPieceLocking=$isPieceLocking")
|
||||
|
||||
currentPiece = nextPiece
|
||||
spawnNextPiece()
|
||||
|
||||
// Center the piece horizontally and spawn one unit higher
|
||||
currentPiece?.apply {
|
||||
x = (width - getWidth()) / 2
|
||||
y = -1 // Spawn one unit above the top of the screen
|
||||
|
||||
Log.d(TAG, "spawnPiece() - new piece spawned at position (${x},${y}), type=${type}")
|
||||
|
||||
// Set the spawn time for the grace period
|
||||
pieceSpawnTime = System.currentTimeMillis()
|
||||
|
||||
// Check if the piece can be placed (Game Over condition)
|
||||
if (!canMove(0, 0)) {
|
||||
isGameOver = true
|
||||
Log.d(TAG, "spawnPiece() - Game Over condition detected")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the current piece left
|
||||
*/
|
||||
fun moveLeft() {
|
||||
if (canMove(-1, 0)) {
|
||||
currentPiece?.x = currentPiece?.x?.minus(1) ?: 0
|
||||
onPieceMove?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the current piece right
|
||||
*/
|
||||
fun moveRight() {
|
||||
if (canMove(1, 0)) {
|
||||
currentPiece?.x = currentPiece?.x?.plus(1) ?: 0
|
||||
onPieceMove?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the current piece down (soft drop)
|
||||
*/
|
||||
fun moveDown(): Boolean {
|
||||
// Don't allow movement if a hard drop is in progress or piece is locking
|
||||
if (isHardDropInProgress || isPieceLocking) return false
|
||||
|
||||
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()) {
|
||||
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 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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()) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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++
|
||||
} 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 Tetris bonus
|
||||
val backToBackMultiplier = if (clearedLines == 4 && lastClearWasTetris) 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
|
||||
lastClearWasTetris = 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 != TetrominoType.T) return false
|
||||
|
||||
// Count occupied corners around the T piece
|
||||
var occupiedCorners = 0
|
||||
val centerX = piece.x + 1
|
||||
val centerY = piece.y + 1
|
||||
|
||||
// Check all four corners
|
||||
if (isOccupied(centerX - 1, centerY - 1)) occupiedCorners++
|
||||
if (isOccupied(centerX + 1, centerY - 1)) occupiedCorners++
|
||||
if (isOccupied(centerX - 1, centerY + 1)) occupiedCorners++
|
||||
if (isOccupied(centerX + 1, centerY + 1)) occupiedCorners++
|
||||
|
||||
// T-Spin requires at least 3 occupied corners
|
||||
return occupiedCorners >= 3
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ghost piece position (preview of where piece will land)
|
||||
*/
|
||||
fun getGhostY(): Int {
|
||||
val piece = currentPiece ?: return 0
|
||||
var ghostY = piece.y
|
||||
|
||||
// Find how far the piece can move down
|
||||
while (true) {
|
||||
if (canMove(0, ghostY - piece.y + 1)) {
|
||||
ghostY++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure ghostY doesn't exceed the board height
|
||||
return ghostY.coerceAtMost(height - 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current tetromino
|
||||
*/
|
||||
fun getCurrentPiece(): Tetromino? = currentPiece
|
||||
|
||||
/**
|
||||
* Check if a cell in the grid is occupied
|
||||
*/
|
||||
fun isOccupied(x: Int, y: Int): Boolean {
|
||||
return if (x in 0 until width && y in 0 until height) {
|
||||
grid[y][x]
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a line is completely filled
|
||||
*/
|
||||
fun isLineFull(y: Int): Boolean {
|
||||
return if (y in 0 until height) {
|
||||
grid[y].all { it }
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current level and adjust game parameters
|
||||
*/
|
||||
fun updateLevel(newLevel: Int) {
|
||||
lastLevel = level
|
||||
level = newLevel.coerceIn(1, 20)
|
||||
startingLevel = level // Store the starting level
|
||||
dropInterval = getDropIntervalForLevel(level)
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new game
|
||||
*/
|
||||
fun startGame() {
|
||||
reset()
|
||||
// Initialize pieces
|
||||
spawnNextPiece()
|
||||
spawnPiece()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the game board
|
||||
*/
|
||||
fun reset() {
|
||||
// Clear the grid
|
||||
for (y in 0 until height) {
|
||||
for (x in 0 until width) {
|
||||
grid[y][x] = false
|
||||
}
|
||||
}
|
||||
|
||||
// Reset game state
|
||||
score = 0
|
||||
level = startingLevel // Use starting level instead of resetting to 1
|
||||
lastLevel = level // Reset lastLevel to match the current level
|
||||
lines = 0
|
||||
isGameOver = false
|
||||
dropInterval = getDropIntervalForLevel(level) // Use helper method
|
||||
|
||||
// Reset scoring state
|
||||
combo = 0
|
||||
lastClearWasTetris = 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
|
||||
*/
|
||||
}
|
8
app/src/main/java/com/pixelmintdrop/model/HighScore.kt
Normal file
8
app/src/main/java/com/pixelmintdrop/model/HighScore.kt
Normal file
|
@ -0,0 +1,8 @@
|
|||
package com.pixelmintdrop.model
|
||||
|
||||
data class HighScore(
|
||||
val name: String,
|
||||
val score: Int,
|
||||
val level: Int,
|
||||
val date: Long = System.currentTimeMillis()
|
||||
)
|
|
@ -0,0 +1,67 @@
|
|||
package com.pixelmintdrop.model
|
||||
|
||||
import android.graphics.Color
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.pixelmintdrop.R
|
||||
|
||||
class HighScoreAdapter : RecyclerView.Adapter<HighScoreAdapter.HighScoreViewHolder>() {
|
||||
private var highScores: List<HighScore> = emptyList()
|
||||
private var currentTheme = "theme_classic" // Default theme
|
||||
private var textColor = Color.WHITE // Default text color
|
||||
|
||||
fun updateHighScores(newHighScores: List<HighScore>) {
|
||||
highScores = newHighScores
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HighScoreViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_high_score, parent, false)
|
||||
return HighScoreViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: HighScoreViewHolder, position: Int) {
|
||||
val highScore = highScores[position]
|
||||
holder.bind(highScore, position + 1)
|
||||
|
||||
// Apply current text color to elements
|
||||
holder.rankText.setTextColor(textColor)
|
||||
holder.nameText.setTextColor(textColor)
|
||||
holder.scoreText.setTextColor(textColor)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = highScores.size
|
||||
|
||||
class HighScoreViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val rankText: TextView = itemView.findViewById(R.id.rankText)
|
||||
val nameText: TextView = itemView.findViewById(R.id.nameText)
|
||||
val scoreText: TextView = itemView.findViewById(R.id.scoreText)
|
||||
|
||||
fun bind(highScore: HighScore, rank: Int) {
|
||||
rankText.text = "#$rank"
|
||||
nameText.text = highScore.name
|
||||
scoreText.text = highScore.score.toString()
|
||||
}
|
||||
}
|
||||
|
||||
fun applyTheme(themeId: String) {
|
||||
currentTheme = themeId
|
||||
|
||||
// Update text color based on theme
|
||||
textColor = when (themeId) {
|
||||
"theme_classic" -> Color.WHITE
|
||||
"theme_neon" -> Color.parseColor("#FF00FF")
|
||||
"theme_monochrome" -> Color.LTGRAY
|
||||
"theme_retro" -> Color.parseColor("#FF5A5F")
|
||||
"theme_minimalist" -> Color.BLACK
|
||||
"theme_galaxy" -> Color.parseColor("#66FCF1")
|
||||
else -> Color.WHITE
|
||||
}
|
||||
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package com.pixelmintdrop.model
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class HighScoreManager(private val context: Context) {
|
||||
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
private val gson = Gson()
|
||||
private val type: Type = object : TypeToken<List<HighScore>>() {}.type
|
||||
|
||||
companion object {
|
||||
private const val PREFS_NAME = "pixelmintdrop_highscores"
|
||||
private const val KEY_HIGHSCORES = "highscores"
|
||||
private const val MAX_HIGHSCORES = 5
|
||||
}
|
||||
|
||||
fun getHighScores(): List<HighScore> {
|
||||
val json = prefs.getString(KEY_HIGHSCORES, null)
|
||||
return if (json != null) {
|
||||
gson.fromJson(json, type)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun addHighScore(highScore: HighScore) {
|
||||
val currentScores = getHighScores().toMutableList()
|
||||
currentScores.add(highScore)
|
||||
|
||||
// Sort by score (descending) and keep only top 5
|
||||
currentScores.sortByDescending { it.score }
|
||||
val topScores = currentScores.take(MAX_HIGHSCORES)
|
||||
|
||||
// Save to SharedPreferences
|
||||
val json = gson.toJson(topScores)
|
||||
prefs.edit().putString(KEY_HIGHSCORES, json).apply()
|
||||
}
|
||||
|
||||
fun isHighScore(score: Int): Boolean {
|
||||
val currentScores = getHighScores()
|
||||
return currentScores.size < MAX_HIGHSCORES ||
|
||||
score > (currentScores.lastOrNull()?.score ?: 0)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,370 @@
|
|||
package com.pixelmintdrop.model
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Manages player progression, experience points, and unlockable rewards
|
||||
*/
|
||||
class PlayerProgressionManager(context: Context) {
|
||||
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
// Player level and XP
|
||||
private var playerLevel: Int = 1
|
||||
private var playerXP: Long = 0
|
||||
private var totalXPEarned: Long = 0
|
||||
|
||||
// Track unlocked rewards
|
||||
private val unlockedThemes = mutableSetOf<String>()
|
||||
private val unlockedBlocks = mutableSetOf<String>()
|
||||
private val unlockedBadges = mutableSetOf<String>()
|
||||
|
||||
// XP gained in the current session
|
||||
private var sessionXPGained: Long = 0
|
||||
|
||||
init {
|
||||
loadProgress()
|
||||
}
|
||||
|
||||
/**
|
||||
* Load player progression data from shared preferences
|
||||
*/
|
||||
private fun loadProgress() {
|
||||
playerLevel = prefs.getInt(KEY_PLAYER_LEVEL, 1)
|
||||
playerXP = prefs.getLong(KEY_PLAYER_XP, 0)
|
||||
totalXPEarned = prefs.getLong(KEY_TOTAL_XP_EARNED, 0)
|
||||
|
||||
// Load unlocked rewards
|
||||
val themesSet = prefs.getStringSet(KEY_UNLOCKED_THEMES, setOf()) ?: setOf()
|
||||
val blocksSet = prefs.getStringSet(KEY_UNLOCKED_BLOCKS, setOf()) ?: setOf()
|
||||
val badgesSet = prefs.getStringSet(KEY_UNLOCKED_BADGES, setOf()) ?: setOf()
|
||||
|
||||
unlockedThemes.addAll(themesSet)
|
||||
unlockedBlocks.addAll(blocksSet)
|
||||
unlockedBadges.addAll(badgesSet)
|
||||
|
||||
// Add default theme if nothing is unlocked
|
||||
if (unlockedThemes.isEmpty()) {
|
||||
unlockedThemes.add(THEME_CLASSIC)
|
||||
}
|
||||
|
||||
// Always ensure default block skin is present (Level 1)
|
||||
unlockedBlocks.add("block_skin_1")
|
||||
|
||||
// Explicitly check and save all unlocks for the current level on load
|
||||
checkAllUnlocksForCurrentLevel()
|
||||
}
|
||||
|
||||
/**
|
||||
* Save player progression data to shared preferences
|
||||
*/
|
||||
private fun saveProgress() {
|
||||
prefs.edit()
|
||||
.putInt(KEY_PLAYER_LEVEL, playerLevel)
|
||||
.putLong(KEY_PLAYER_XP, playerXP)
|
||||
.putLong(KEY_TOTAL_XP_EARNED, totalXPEarned)
|
||||
.putStringSet(KEY_UNLOCKED_THEMES, unlockedThemes)
|
||||
.putStringSet(KEY_UNLOCKED_BLOCKS, unlockedBlocks)
|
||||
.putStringSet(KEY_UNLOCKED_BADGES, unlockedBadges)
|
||||
.apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate XP required to reach a specific level
|
||||
*/
|
||||
fun calculateXPForLevel(level: Int): Long {
|
||||
return (BASE_XP * level.toDouble().pow(XP_CURVE_FACTOR)).roundToInt().toLong()
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate total XP required to reach a certain level from level 1
|
||||
*/
|
||||
fun calculateTotalXPForLevel(level: Int): Long {
|
||||
var totalXP = 0L
|
||||
for (lvl in 1 until level) {
|
||||
totalXP += calculateXPForLevel(lvl)
|
||||
}
|
||||
return totalXP
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate XP from a game session based on score, lines, level, etc.
|
||||
*/
|
||||
fun calculateGameXP(score: Int, lines: Int, level: Int, gameTime: Long,
|
||||
tetrisCount: Int, perfectClearCount: Int): Long {
|
||||
// Base XP from score with level multiplier (capped at level 10)
|
||||
val cappedLevel = min(level, 10)
|
||||
val scoreXP = (score * (1 + LEVEL_MULTIPLIER * cappedLevel)).toLong()
|
||||
|
||||
// XP from lines cleared (reduced for higher levels)
|
||||
val linesXP = lines * XP_PER_LINE * (1 - (level - 1) * 0.05).coerceAtLeast(0.5)
|
||||
|
||||
// XP from special moves (reduced for higher levels)
|
||||
val tetrisBonus = tetrisCount * TETRIS_XP_BONUS * (1 - (level - 1) * 0.05).coerceAtLeast(0.5)
|
||||
val perfectClearBonus = perfectClearCount * PERFECT_CLEAR_XP_BONUS * (1 - (level - 1) * 0.05).coerceAtLeast(0.5)
|
||||
|
||||
// Time bonus (reduced for longer games)
|
||||
val timeBonus = (gameTime / 60000) * TIME_XP_PER_MINUTE * (1 - (gameTime / 3600000) * 0.1).coerceAtLeast(0.5)
|
||||
|
||||
// Calculate total XP
|
||||
return (scoreXP + linesXP + tetrisBonus + perfectClearBonus + timeBonus).toLong()
|
||||
}
|
||||
|
||||
/**
|
||||
* Add XP to the player and handle level-ups
|
||||
* Returns a list of newly unlocked rewards
|
||||
*/
|
||||
fun addXP(xpAmount: Long): List<String> {
|
||||
sessionXPGained = xpAmount
|
||||
playerXP += xpAmount
|
||||
totalXPEarned += xpAmount
|
||||
|
||||
val newRewards = mutableListOf<String>()
|
||||
val oldLevel = playerLevel
|
||||
|
||||
// Check for level ups
|
||||
var xpForNextLevel = calculateXPForLevel(playerLevel)
|
||||
while (playerXP >= xpForNextLevel) {
|
||||
playerXP -= xpForNextLevel
|
||||
playerLevel++
|
||||
|
||||
// Check for new rewards at this level
|
||||
val levelRewards = checkLevelRewards(playerLevel)
|
||||
newRewards.addAll(levelRewards)
|
||||
|
||||
// Calculate XP needed for the next level
|
||||
xpForNextLevel = calculateXPForLevel(playerLevel)
|
||||
}
|
||||
|
||||
// Save progress if there were any changes
|
||||
if (oldLevel != playerLevel || newRewards.isNotEmpty()) {
|
||||
saveProgress()
|
||||
}
|
||||
|
||||
return newRewards
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the player unlocked new rewards at the current level
|
||||
*/
|
||||
private fun checkLevelRewards(level: Int): List<String> {
|
||||
val newRewards = mutableListOf<String>()
|
||||
|
||||
// Check for theme unlocks
|
||||
when (level) {
|
||||
5 -> {
|
||||
if (unlockedThemes.add(THEME_NEON)) {
|
||||
newRewards.add("Unlocked Neon Theme!")
|
||||
}
|
||||
}
|
||||
10 -> {
|
||||
if (unlockedThemes.add(THEME_MONOCHROME)) {
|
||||
newRewards.add("Unlocked Monochrome Theme!")
|
||||
}
|
||||
}
|
||||
15 -> {
|
||||
if (unlockedThemes.add(THEME_RETRO)) {
|
||||
newRewards.add("Unlocked Retro Arcade Theme!")
|
||||
}
|
||||
}
|
||||
20 -> {
|
||||
if (unlockedThemes.add(THEME_MINIMALIST)) {
|
||||
newRewards.add("Unlocked Minimalist Theme!")
|
||||
}
|
||||
}
|
||||
25 -> {
|
||||
if (unlockedThemes.add(THEME_GALAXY)) {
|
||||
newRewards.add("Unlocked Galaxy Theme!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for block skin unlocks (start from skin 2 at level 7)
|
||||
when (level) {
|
||||
7 -> {
|
||||
if (unlockedBlocks.add("block_skin_2")) {
|
||||
newRewards.add("Unlocked Neon Block Skin!")
|
||||
}
|
||||
}
|
||||
14 -> {
|
||||
if (unlockedBlocks.add("block_skin_3")) {
|
||||
newRewards.add("Unlocked Retro Block Skin!")
|
||||
}
|
||||
}
|
||||
21 -> {
|
||||
if (unlockedBlocks.add("block_skin_4")) {
|
||||
newRewards.add("Unlocked Minimalist Block Skin!")
|
||||
}
|
||||
}
|
||||
28 -> {
|
||||
if (unlockedBlocks.add("block_skin_5")) {
|
||||
newRewards.add("Unlocked Galaxy Block Skin!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newRewards
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and unlock any rewards the player should have based on their current level
|
||||
* This ensures players don't miss unlocks if they level up multiple times at once
|
||||
*/
|
||||
private fun checkAllUnlocksForCurrentLevel() {
|
||||
// Check theme unlocks
|
||||
if (playerLevel >= 5) unlockedThemes.add(THEME_NEON)
|
||||
if (playerLevel >= 10) unlockedThemes.add(THEME_MONOCHROME)
|
||||
if (playerLevel >= 15) unlockedThemes.add(THEME_RETRO)
|
||||
if (playerLevel >= 20) unlockedThemes.add(THEME_MINIMALIST)
|
||||
if (playerLevel >= 25) unlockedThemes.add(THEME_GALAXY)
|
||||
|
||||
// Check block skin unlocks (start from skin 2 at level 7)
|
||||
// Skin 1 is default (added in loadProgress)
|
||||
if (playerLevel >= 7) unlockedBlocks.add("block_skin_2")
|
||||
if (playerLevel >= 14) unlockedBlocks.add("block_skin_3")
|
||||
if (playerLevel >= 21) unlockedBlocks.add("block_skin_4")
|
||||
if (playerLevel >= 28) unlockedBlocks.add("block_skin_5")
|
||||
|
||||
// Save any newly unlocked items
|
||||
saveProgress()
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new progression session
|
||||
*/
|
||||
fun startNewSession() {
|
||||
sessionXPGained = 0
|
||||
|
||||
// Ensure all appropriate unlocks are available
|
||||
checkAllUnlocksForCurrentLevel()
|
||||
}
|
||||
|
||||
// Getters
|
||||
fun getPlayerLevel(): Int = playerLevel
|
||||
fun getCurrentXP(): Long = playerXP
|
||||
fun getXPForNextLevel(): Long = calculateXPForLevel(playerLevel)
|
||||
fun getSessionXPGained(): Long = sessionXPGained
|
||||
fun getUnlockedThemes(): Set<String> = unlockedThemes.toSet()
|
||||
fun getUnlockedBlocks(): Set<String> = unlockedBlocks.toSet()
|
||||
fun getUnlockedBadges(): Set<String> = unlockedBadges.toSet()
|
||||
|
||||
/**
|
||||
* Check if a specific theme is unlocked
|
||||
*/
|
||||
fun isThemeUnlocked(themeId: String): Boolean {
|
||||
return unlockedThemes.contains(themeId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Award a badge to the player
|
||||
*/
|
||||
fun awardBadge(badgeId: String): Boolean {
|
||||
val newlyAwarded = unlockedBadges.add(badgeId)
|
||||
if (newlyAwarded) {
|
||||
saveProgress()
|
||||
}
|
||||
return newlyAwarded
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all player progression data
|
||||
*/
|
||||
fun resetProgress() {
|
||||
playerLevel = 1
|
||||
playerXP = 0
|
||||
totalXPEarned = 0
|
||||
|
||||
unlockedThemes.clear()
|
||||
unlockedBlocks.clear()
|
||||
unlockedBadges.clear()
|
||||
|
||||
// Add default theme
|
||||
unlockedThemes.add(THEME_CLASSIC)
|
||||
|
||||
// Add default block skin (Level 1)
|
||||
unlockedBlocks.add("block_skin_1")
|
||||
|
||||
saveProgress()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREFS_NAME = "pixelmintdrop_progression"
|
||||
private const val KEY_PLAYER_LEVEL = "player_level"
|
||||
private const val KEY_PLAYER_XP = "player_xp"
|
||||
private const val KEY_TOTAL_XP_EARNED = "total_xp_earned"
|
||||
private const val KEY_UNLOCKED_THEMES = "unlocked_themes"
|
||||
private const val KEY_UNLOCKED_BLOCKS = "unlocked_blocks"
|
||||
private const val KEY_UNLOCKED_BADGES = "unlocked_badges"
|
||||
private const val KEY_SELECTED_THEME = "selected_theme"
|
||||
private const val KEY_SELECTED_BLOCK_SKIN = "selected_block_skin"
|
||||
|
||||
// XP constants
|
||||
private const val BASE_XP = 3000L
|
||||
private const val XP_CURVE_FACTOR = 2.0
|
||||
private const val LEVEL_MULTIPLIER = 0.03
|
||||
private const val XP_PER_LINE = 40L
|
||||
private const val TETRIS_XP_BONUS = 150L
|
||||
private const val PERFECT_CLEAR_XP_BONUS = 300L
|
||||
private const val TIME_XP_PER_MINUTE = 20L
|
||||
|
||||
// Theme constants
|
||||
const val THEME_CLASSIC = "theme_classic"
|
||||
const val THEME_NEON = "theme_neon"
|
||||
const val THEME_MONOCHROME = "theme_monochrome"
|
||||
const val THEME_RETRO = "theme_retro"
|
||||
const val THEME_MINIMALIST = "theme_minimalist"
|
||||
const val THEME_GALAXY = "theme_galaxy"
|
||||
|
||||
// Map of themes to required levels
|
||||
val THEME_REQUIRED_LEVELS = mapOf(
|
||||
THEME_CLASSIC to 1,
|
||||
THEME_NEON to 5,
|
||||
THEME_MONOCHROME to 10,
|
||||
THEME_RETRO to 15,
|
||||
THEME_MINIMALIST to 20,
|
||||
THEME_GALAXY to 25
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required level for a specific theme
|
||||
*/
|
||||
fun getRequiredLevelForTheme(themeId: String): Int {
|
||||
return THEME_REQUIRED_LEVELS[themeId] ?: 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selected block skin
|
||||
*/
|
||||
fun setSelectedBlockSkin(skinId: String) {
|
||||
if (unlockedBlocks.contains(skinId)) {
|
||||
prefs.edit().putString(KEY_SELECTED_BLOCK_SKIN, skinId).commit()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the selected block skin
|
||||
*/
|
||||
fun getSelectedBlockSkin(): String {
|
||||
return prefs.getString(KEY_SELECTED_BLOCK_SKIN, "block_skin_1") ?: "block_skin_1"
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selected theme
|
||||
*/
|
||||
fun setSelectedTheme(themeId: String) {
|
||||
if (unlockedThemes.contains(themeId)) {
|
||||
prefs.edit().putString(KEY_SELECTED_THEME, themeId).apply()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the selected theme
|
||||
*/
|
||||
fun getSelectedTheme(): String {
|
||||
return prefs.getString(KEY_SELECTED_THEME, THEME_CLASSIC) ?: THEME_CLASSIC
|
||||
}
|
||||
}
|
196
app/src/main/java/com/pixelmintdrop/model/StatsManager.kt
Normal file
196
app/src/main/java/com/pixelmintdrop/model/StatsManager.kt
Normal file
|
@ -0,0 +1,196 @@
|
|||
package com.pixelmintdrop.model
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
|
||||
class StatsManager(context: Context) {
|
||||
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 totalTetrises: Int = 0
|
||||
|
||||
// Session stats
|
||||
private var sessionScore: Int = 0
|
||||
private var sessionLines: Int = 0
|
||||
private var sessionPieces: Int = 0
|
||||
private var sessionTime: Long = 0
|
||||
private var sessionLevel: Int = 0
|
||||
|
||||
// Line clear stats (session)
|
||||
private var sessionSingles: Int = 0
|
||||
private var sessionDoubles: Int = 0
|
||||
private var sessionTriples: Int = 0
|
||||
private var sessionTetrises: Int = 0
|
||||
|
||||
init {
|
||||
loadStats()
|
||||
}
|
||||
|
||||
private fun loadStats() {
|
||||
totalGames = prefs.getInt(KEY_TOTAL_GAMES, 0)
|
||||
totalScore = prefs.getLong(KEY_TOTAL_SCORE, 0)
|
||||
totalLines = prefs.getInt(KEY_TOTAL_LINES, 0)
|
||||
totalPieces = prefs.getInt(KEY_TOTAL_PIECES, 0)
|
||||
totalTime = prefs.getLong(KEY_TOTAL_TIME, 0)
|
||||
maxLevel = prefs.getInt(KEY_MAX_LEVEL, 0)
|
||||
maxScore = prefs.getInt(KEY_MAX_SCORE, 0)
|
||||
maxLines = prefs.getInt(KEY_MAX_LINES, 0)
|
||||
|
||||
// Load line clear stats
|
||||
totalSingles = prefs.getInt(KEY_TOTAL_SINGLES, 0)
|
||||
totalDoubles = prefs.getInt(KEY_TOTAL_DOUBLES, 0)
|
||||
totalTriples = prefs.getInt(KEY_TOTAL_TRIPLES, 0)
|
||||
totalTetrises = prefs.getInt(KEY_TOTAL_TETRISES, 0)
|
||||
}
|
||||
|
||||
private fun saveStats() {
|
||||
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_TETRISES, totalTetrises)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun startNewSession() {
|
||||
sessionScore = 0
|
||||
sessionLines = 0
|
||||
sessionPieces = 0
|
||||
sessionTime = 0
|
||||
sessionLevel = 0
|
||||
sessionSingles = 0
|
||||
sessionDoubles = 0
|
||||
sessionTriples = 0
|
||||
sessionTetrises = 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 -> {
|
||||
sessionTetrises++
|
||||
totalTetrises++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 getTotalTetrises(): Int = totalTetrises
|
||||
|
||||
// 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 getSessionTetrises(): Int = sessionTetrises
|
||||
|
||||
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
|
||||
totalTetrises = 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_TETRISES = "total_tetrises"
|
||||
}
|
||||
}
|
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 Tetris 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)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue