Compare commits

...

5 commits

48 changed files with 990 additions and 1816 deletions

View file

@ -1,11 +1,11 @@
# Mintris
# pixelmintdrop
A modern Tetris implementation for Android, featuring smooth animations, responsive controls, and a beautiful minimalist design.
A modern block-stacking puzzle game for Android, featuring smooth animations, responsive controls, and a beautiful minimalist design.
## Features
### Core Gameplay
- Classic Tetris mechanics
- Classic block-stacking mechanics
- 7-bag randomizer for piece distribution
- Ghost piece preview
- Hard drop and soft drop
@ -29,7 +29,7 @@ The game features a comprehensive scoring system:
- Single line: 40 points
- Double: 100 points
- Triple: 300 points
- Tetris (4 lines): 1200 points
- Quad (4 lines): 1200 points
#### Multipliers
@ -48,15 +48,15 @@ The game features a comprehensive scoring system:
- 4 combos: 2.5x
- 5+ combos: 3.0x
3. **Back-to-Back Tetris**
- 50% bonus (1.5x) for consecutive Tetris clears
- Resets if a non-Tetris clear is performed
3. **Back-to-Back Quad**
- 50% bonus (1.5x) for consecutive quad clears
- Resets if a non-quad clear is performed
4. **Perfect Clear**
- 2x for single line
- 3x for double
- 4x for triple
- 5x for Tetris
- 5x for quad
- Awarded when clearing lines without leaving blocks
5. **All Clear**
@ -219,7 +219,7 @@ private fun onGameOver(score: Long) {
1. Clone the repository:
```bash
git clone https://github.com/cmclark00/mintris.git
git clone https://github.com/cmclark00/pixelmintdrop.git
```
2. Open the project in Android Studio

View file

@ -4,13 +4,13 @@ plugins {
}
android {
namespace 'com.mintris'
compileSdk 34
namespace "com.pixelmintdrop"
compileSdk 35
defaultConfig {
applicationId "com.mintris"
applicationId "com.pixelmintdrop"
minSdk 30
targetSdk 34
targetSdk 35
versionCode 1
versionName "1.0"
@ -27,6 +27,7 @@ android {
buildFeatures {
viewBinding true
dataBinding true
}
compileOptions {

View file

@ -6,17 +6,17 @@
# http://developer.android.com/guide/developing/tools/proguard.html
# Keep models intact
-keep class com.mintris.model.** { *; }
-keep class com.pixelmintdrop.model.** { *; }
# Keep game classes intact to prevent issues
-keep class com.mintris.game.** { *; }
-keep class com.pixelmintdrop.game.** { *; }
# Preserve critical classes that might be used through reflection
-keep class com.mintris.audio.GameMusic { *; }
-keep class com.mintris.ui.** { *; }
-keep class com.pixelmintdrop.audio.GameMusic { *; }
-keep class com.pixelmintdrop.ui.** { *; }
# Keep all public methods in the MainActivity
-keepclassmembers class com.mintris.MainActivity {
-keepclassmembers class com.pixelmintdrop.MainActivity {
public *;
}

View file

@ -12,11 +12,11 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Mintris">
android:theme="@style/Theme.pixelmintdrop">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.Mintris.NoActionBar"
android:theme="@style/Theme.pixelmintdrop.NoActionBar"
android:immersive="true"
android:resizeableActivity="false"
android:excludeFromRecents="false"

View file

@ -1,99 +0,0 @@
package com.mintris.game
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.graphics.BlurMaskFilter
import android.util.AttributeSet
import android.view.View
import kotlin.math.min
/**
* Custom view to display the next Tetromino piece
*/
class NextPieceView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var gameView: GameView? = null
// Rendering
private val blockPaint = Paint().apply {
color = Color.WHITE
isAntiAlias = true
}
private val glowPaint = Paint().apply {
color = Color.WHITE
alpha = 30
isAntiAlias = true
style = Paint.Style.STROKE
strokeWidth = 1.5f
}
/**
* Set the game view to get the next piece from
*/
fun setGameView(gameView: GameView) {
this.gameView = gameView
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Get the next piece from game view
gameView?.let {
it.getNextPiece()?.let { piece ->
val width = piece.getWidth()
val height = piece.getHeight()
// Calculate block size for the preview (smaller than main board)
val previewBlockSize = min(
canvas.width.toFloat() / (width + 2),
canvas.height.toFloat() / (height + 2)
)
// Center the piece in the preview area
val previewLeft = (canvas.width - width * previewBlockSize) / 2
val previewTop = (canvas.height - height * previewBlockSize) / 2
// Draw subtle background glow
val glowPaint = Paint().apply {
color = Color.WHITE
alpha = 10
maskFilter = BlurMaskFilter(previewBlockSize * 0.5f, BlurMaskFilter.Blur.OUTER)
}
canvas.drawRect(
previewLeft - previewBlockSize,
previewTop - previewBlockSize,
previewLeft + width * previewBlockSize + previewBlockSize,
previewTop + height * previewBlockSize + previewBlockSize,
glowPaint
)
for (y in 0 until height) {
for (x in 0 until width) {
if (piece.isBlockAt(x, y)) {
val left = previewLeft + x * previewBlockSize
val top = previewTop + y * previewBlockSize
val right = left + previewBlockSize
val bottom = top + previewBlockSize
// Draw block with subtle glow
val rect = RectF(left + 1, top + 1, right - 1, bottom - 1)
canvas.drawRect(rect, blockPaint)
// Draw subtle border glow
val glowRect = RectF(left, top, right, bottom)
canvas.drawRect(glowRect, glowPaint)
}
}
}
}
}
}
}

View file

@ -1,765 +0,0 @@
package com.mintris.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
*/
}

View file

@ -1,196 +0,0 @@
package com.mintris.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 = "mintris_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"
}
}

View file

@ -1,16 +1,12 @@
package com.mintris
package com.pixelmintdrop
import android.app.Activity
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.mintris.databinding.HighScoreEntryBinding
import com.mintris.model.HighScore
import com.mintris.model.HighScoreManager
import com.mintris.model.PlayerProgressionManager
import com.pixelmintdrop.databinding.HighScoreEntryBinding
import com.pixelmintdrop.model.HighScore
import com.pixelmintdrop.model.HighScoreManager
import com.pixelmintdrop.model.PlayerProgressionManager
import android.graphics.Color
import android.view.KeyEvent
import android.view.InputDevice

View file

@ -1,15 +1,12 @@
package com.mintris
package com.pixelmintdrop
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.mintris.databinding.HighScoresBinding
import com.mintris.model.HighScoreAdapter
import com.mintris.model.HighScoreManager
import com.mintris.model.PlayerProgressionManager
import com.pixelmintdrop.databinding.HighScoresBinding
import com.pixelmintdrop.model.HighScoreAdapter
import com.pixelmintdrop.model.HighScoreManager
import com.pixelmintdrop.model.PlayerProgressionManager
import android.graphics.Color
import android.util.Log
import android.view.KeyEvent

View file

@ -1,4 +1,4 @@
package com.mintris
package com.pixelmintdrop
import android.content.Context
import android.content.Intent
@ -9,18 +9,18 @@ import android.os.Vibrator
import android.view.View
import android.view.HapticFeedbackConstants
import androidx.appcompat.app.AppCompatActivity
import com.mintris.databinding.ActivityMainBinding
import com.mintris.game.GameHaptics
import com.mintris.game.GameView
import com.mintris.game.TitleScreen
import com.mintris.model.GameBoard
import com.mintris.audio.GameMusic
import com.mintris.model.HighScoreManager
import com.mintris.model.PlayerProgressionManager
import com.mintris.model.StatsManager
import com.mintris.ui.ProgressionScreen
import com.mintris.ui.ThemeSelector
import com.mintris.ui.BlockSkinSelector
import com.pixelmintdrop.databinding.ActivityMainBinding
import com.pixelmintdrop.game.GameHaptics
import com.pixelmintdrop.game.GameView
import com.pixelmintdrop.game.TitleScreen
import com.pixelmintdrop.model.GameBoard
import com.pixelmintdrop.audio.GameMusic
import com.pixelmintdrop.model.HighScoreManager
import com.pixelmintdrop.model.PlayerProgressionManager
import com.pixelmintdrop.model.StatsManager
import com.pixelmintdrop.ui.ProgressionScreen
import com.pixelmintdrop.ui.ThemeSelector
import com.pixelmintdrop.ui.BlockSkinSelector
import java.text.SimpleDateFormat
import java.util.*
import android.graphics.Color
@ -32,12 +32,11 @@ import android.view.KeyEvent
import android.os.Handler
import android.os.Looper
import android.view.MotionEvent
import com.mintris.game.GamepadController
import com.pixelmintdrop.game.GamepadController
import android.view.InputDevice
import android.widget.Toast
import android.content.BroadcastReceiver
import android.content.IntentFilter
import android.app.AlertDialog
import android.graphics.drawable.ColorDrawable
import androidx.core.content.ContextCompat
import android.widget.Button
@ -153,7 +152,7 @@ class MainActivity : AppCompatActivity(),
pauseMenuScrollView = binding.pauseMenuScrollView
// Load random mode setting
isRandomModeEnabled = getSharedPreferences("com.mintris.preferences", Context.MODE_PRIVATE)
isRandomModeEnabled = getSharedPreferences("com.com.pixelmintgames.pixelmintdrop.preferences", Context.MODE_PRIVATE)
.getBoolean("random_mode_enabled", false)
// Initialize gamepad controller
@ -302,7 +301,14 @@ class MainActivity : AppCompatActivity(),
statsManager.updateSessionStats(finalScore, gameBoard.lines, piecesPlaced, timePlayedMs, currentLevel)
// Handle progression - XP earned, potential level up
val xpGained = progressionManager.calculateGameXP(finalScore, gameBoard.lines, currentLevel, timePlayedMs, statsManager.getSessionTetrises(), 0)
val xpGained = progressionManager.calculateGameXP(
score = finalScore,
lines = gameBoard.lines,
level = currentLevel,
timePlayedMs = timePlayedMs,
quadCount = statsManager.getSessionQuads(),
perfectClearCount = statsManager.getSessionPerfectClears()
)
val newRewards = progressionManager.addXP(xpGained)
// Show progression screen if player earned XP
@ -472,9 +478,9 @@ class MainActivity : AppCompatActivity(),
score = score,
lines = gameBoard.lines,
level = currentLevel,
gameTime = gameTime,
tetrisCount = statsManager.getSessionTetrises(),
perfectClearCount = 0 // Implement perfect clear tracking if needed
timePlayedMs = gameTime,
quadCount = statsManager.getSessionQuads(),
perfectClearCount = statsManager.getSessionPerfectClears()
)
// Add XP and check for rewards
@ -497,7 +503,7 @@ class MainActivity : AppCompatActivity(),
binding.sessionSinglesText.text = getString(R.string.singles, statsManager.getSessionSingles())
binding.sessionDoublesText.text = getString(R.string.doubles, statsManager.getSessionDoubles())
binding.sessionTriplesText.text = getString(R.string.triples, statsManager.getSessionTriples())
binding.sessionTetrisesText.text = getString(R.string.tetrises, statsManager.getSessionTetrises())
binding.sessionQuadsText.text = getString(R.string.quads, statsManager.getSessionQuads())
// Flag to track if high score screen will be shown
var showingHighScore = false
@ -624,7 +630,7 @@ class MainActivity : AppCompatActivity(),
isEnabled = progressionManager.getPlayerLevel() >= 5
setOnCheckedChangeListener { _, isChecked ->
isRandomModeEnabled = isChecked
getSharedPreferences("com.mintris.preferences", Context.MODE_PRIVATE)
getSharedPreferences("com.com.pixelmintgames.pixelmintdrop.preferences", Context.MODE_PRIVATE)
.edit()
.putBoolean("random_mode_enabled", isChecked)
.apply()
@ -912,7 +918,14 @@ class MainActivity : AppCompatActivity(),
statsManager.updateSessionStats(finalScore, gameBoard.lines, piecesPlaced, timePlayedMs, currentLevel)
// Handle progression - XP earned, potential level up
val xpGained = progressionManager.calculateGameXP(finalScore, gameBoard.lines, currentLevel, timePlayedMs, statsManager.getSessionTetrises(), 0)
val xpGained = progressionManager.calculateGameXP(
score = finalScore,
lines = gameBoard.lines,
level = currentLevel,
timePlayedMs = timePlayedMs,
quadCount = statsManager.getSessionQuads(),
perfectClearCount = statsManager.getSessionPerfectClears()
)
val newRewards = progressionManager.addXP(xpGained)
// Show progression screen if player earned XP
@ -1211,7 +1224,7 @@ class MainActivity : AppCompatActivity(),
* Check if user has seen the gamepad help
*/
private fun hasSeenGamepadHelp(): Boolean {
val prefs = getSharedPreferences("com.mintris.preferences", Context.MODE_PRIVATE)
val prefs = getSharedPreferences("com.com.pixelmintgames.pixelmintdrop.preferences", Context.MODE_PRIVATE)
return prefs.getBoolean("has_seen_gamepad_help", false)
}
@ -1219,7 +1232,7 @@ class MainActivity : AppCompatActivity(),
* Mark that user has seen the gamepad help
*/
private fun markGamepadHelpSeen() {
val prefs = getSharedPreferences("com.mintris.preferences", Context.MODE_PRIVATE)
val prefs = getSharedPreferences("com.com.pixelmintgames.pixelmintdrop.preferences", Context.MODE_PRIVATE)
prefs.edit().putBoolean("has_seen_gamepad_help", true).apply()
}

View file

@ -1,13 +1,11 @@
package com.mintris
package com.pixelmintdrop
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.mintris.databinding.ActivityStatsBinding
import com.mintris.model.StatsManager
import com.mintris.model.PlayerProgressionManager
import com.pixelmintdrop.databinding.ActivityStatsBinding
import com.pixelmintdrop.model.StatsManager
import com.pixelmintdrop.model.PlayerProgressionManager
import android.graphics.Color
import java.text.SimpleDateFormat
import java.util.*
@ -80,7 +78,7 @@ class StatsActivity : AppCompatActivity() {
binding.totalSinglesText.setTextColor(textColor)
binding.totalDoublesText.setTextColor(textColor)
binding.totalTriplesText.setTextColor(textColor)
binding.totalTetrisesText.setTextColor(textColor)
binding.totalQuadsText.setTextColor(textColor)
binding.maxLevelText.setTextColor(textColor)
binding.maxScoreText.setTextColor(textColor)
binding.maxLinesText.setTextColor(textColor)
@ -118,7 +116,7 @@ class StatsActivity : AppCompatActivity() {
binding.totalSinglesText.text = getString(R.string.singles, statsManager.getTotalSingles())
binding.totalDoublesText.text = getString(R.string.doubles, statsManager.getTotalDoubles())
binding.totalTriplesText.text = getString(R.string.triples, statsManager.getTotalTriples())
binding.totalTetrisesText.text = getString(R.string.tetrises, statsManager.getTotalTetrises())
binding.totalQuadsText.text = getString(R.string.quads, statsManager.getTotalQuads())
// Update best performance stats
binding.maxLevelText.text = getString(R.string.max_level, statsManager.getMaxLevel())

View file

@ -1,4 +1,4 @@
package com.mintris
package com.pixelmintdrop
import android.graphics.Color

View file

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

View file

@ -1,11 +1,11 @@
package com.mintris.audio
package com.pixelmintdrop.audio
import android.content.Context
import android.media.MediaPlayer
import android.media.AudioAttributes
import android.os.Build
import android.util.Log
import com.mintris.R
import com.pixelmintdrop.R
class GameMusic(private val context: Context) {
private var mediaPlayer: MediaPlayer? = null

View file

@ -1,4 +1,4 @@
package com.mintris.game
package com.pixelmintdrop.game
import android.content.Context
import android.os.Build
@ -53,7 +53,7 @@ class GameHaptics(private val context: Context) {
1 -> (50L * multiplier).toLong() // Single line: short vibration
2 -> (80L * multiplier).toLong() // Double line: slightly longer
3 -> (120L * multiplier).toLong() // Triple line: even longer
4 -> (200L * multiplier).toLong() // Tetris: longest vibration
4 -> (200L * multiplier).toLong() // Quad: longest vibration
else -> (50L * multiplier).toLong()
}
@ -61,7 +61,7 @@ class GameHaptics(private val context: Context) {
1 -> (80 * multiplier).toInt().coerceAtMost(255) // Single line: mild vibration
2 -> (120 * multiplier).toInt().coerceAtMost(255) // Double line: medium vibration
3 -> (180 * multiplier).toInt().coerceAtMost(255) // Triple line: strong vibration
4 -> 255 // Tetris: maximum vibration
4 -> 255 // Quad: maximum vibration
else -> (80 * multiplier).toInt().coerceAtMost(255)
}

View file

@ -1,13 +1,13 @@
package com.mintris.game
package com.pixelmintdrop.game
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import com.google.gson.Gson
import com.mintris.audio.GameMusic
import com.mintris.model.GameBoard
import com.mintris.model.HighScoreManager
import com.mintris.model.StatsManager
import com.pixelmintdrop.audio.GameMusic
import com.pixelmintdrop.model.GameBoard
import com.pixelmintdrop.model.HighScoreManager
import com.pixelmintdrop.model.StatsManager
/**
* Handles the game's lifecycle events to ensure proper resource management
@ -15,7 +15,7 @@ import com.mintris.model.StatsManager
*/
class GameLifecycleManager(private val context: Context) {
private val sharedPreferences: SharedPreferences =
context.getSharedPreferences("com.mintris.game_state", Context.MODE_PRIVATE)
context.getSharedPreferences("com.com.pixelmintgames.pixelmintdrop.game_state", Context.MODE_PRIVATE)
private val gson = Gson()
/**

View file

@ -1,4 +1,4 @@
package com.mintris.game
package com.pixelmintdrop.game
import android.animation.ValueAnimator
import android.content.Context
@ -20,14 +20,19 @@ import android.view.View
import android.view.animation.LinearInterpolator
import android.hardware.display.DisplayManager
import android.view.Display
import com.mintris.model.GameBoard
import com.mintris.model.Tetromino
import com.mintris.model.TetrominoType
import com.pixelmintdrop.model.GameBoard
import com.pixelmintdrop.model.GamePiece
import com.pixelmintdrop.model.GamePieceType
import com.pixelmintdrop.model.PlayerProgressionManager
import com.pixelmintdrop.model.StatsManager
import com.pixelmintdrop.model.ThemeManager
import com.pixelmintdrop.model.ThemeManager.Theme
import com.pixelmintdrop.model.ThemeManager.ThemeType
import com.pixelmintdrop.model.ThemeManager.ThemeVariant
import kotlin.math.abs
import kotlin.math.min
/**
* GameView that renders the Tetris game and handles touch input
* GameView that renders the block-stacking game and handles touch input
*/
class GameView @JvmOverloads constructor(
context: Context,
@ -45,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
@ -68,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
@ -98,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() {
@ -127,7 +132,6 @@ class GameView @JvmOverloads constructor(
}
}
// Touch parameters
private var lastTouchX = 0f
private var lastTouchY = 0f
private var startX = 0f
@ -216,7 +220,7 @@ class GameView @JvmOverloads constructor(
pause()
// Load saved block skin
val prefs = context.getSharedPreferences("mintris_progression", Context.MODE_PRIVATE)
val prefs = context.getSharedPreferences("pixelmintdrop_progression", Context.MODE_PRIVATE)
currentBlockSkin = prefs.getString("selected_block_skin", "block_skin_1") ?: "block_skin_1"
// Connect our callbacks to the GameBoard
@ -329,7 +333,7 @@ class GameView @JvmOverloads constructor(
}
// Save the selection to SharedPreferences
val prefs = context.getSharedPreferences("mintris_progression", Context.MODE_PRIVATE)
val prefs = context.getSharedPreferences("pixelmintdrop_progression", Context.MODE_PRIVATE)
prefs.edit().putString("selected_block_skin", skinId).commit()
// Force a refresh of the view
@ -428,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)
}
}
@ -776,219 +612,52 @@ class GameView @JvmOverloads constructor(
* Draw the ghost piece (landing preview)
*/
private fun drawGhostPiece(canvas: Canvas) {
val piece = gameBoard.getCurrentPiece() ?: return
val ghostY = gameBoard.getGhostY()
// Draw semi-transparent background for each block
for (y in 0 until piece.getHeight()) {
for (x in 0 until piece.getWidth()) {
if (piece.isBlockAt(x, y)) {
val boardX = piece.x + x
val boardY = ghostY + y
if (boardX >= 0 && boardX < gameBoard.width) {
val screenX = boardLeft + boardX * blockSize
val screenY = boardTop + boardY * blockSize
// Draw background
canvas.drawRect(
screenX + 1f,
screenY + 1f,
screenX + blockSize - 1f,
screenY + blockSize - 1f,
ghostBackgroundPaint
)
// Draw border
canvas.drawRect(
screenX + 1f,
screenY + 1f,
screenX + blockSize - 1f,
screenY + blockSize - 1f,
ghostBorderPaint
)
// Draw outline
canvas.drawRect(
screenX + 1f,
screenY + 1f,
screenX + blockSize - 1f,
screenY + blockSize - 1f,
ghostPaint
)
}
}
}
currentPiece?.let { piece ->
val ghostY = calculateGhostY(piece)
val originalY = piece.y
piece.y = ghostY
ghostBlockPaint.alpha = 50
drawPiece(canvas, piece, alpha = 50)
piece.y = originalY
}
}
/**
* Draw a single tetris block at the given grid position
*/
private fun drawBlock(canvas: Canvas, x: Int, y: Int, isGhost: Boolean, isPulsingLine: Boolean) {
val left = boardLeft + x * blockSize
val top = boardTop + y * blockSize
val right = left + blockSize
val bottom = top + blockSize
private fun calculateGhostY(piece: GamePiece): Int {
var testY = piece.y
while (testY < gameBoard.height && !gameBoard.wouldCollide(piece, piece.x, testY + 1)) {
testY++
}
return testY
}
private fun drawPiece(canvas: Canvas, piece: GamePiece, offsetX: Float = 0f, offsetY: Float = 0f, alpha: Int = 255) {
val width = piece.getWidth()
val height = piece.getHeight()
// Save canvas state before drawing block effects
canvas.save()
// Get the current block skin paint
val paint = blockSkinPaints[currentBlockSkin] ?: blockSkinPaints["block_skin_1"]!!
// Create a clone of the paint to avoid modifying the original
val blockPaint = Paint(paint)
// Draw block based on current skin
when (currentBlockSkin) {
"block_skin_1" -> { // Classic
// Draw outer glow
blockGlowPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE
canvas.drawRect(left - 2f, top - 2f, right + 2f, bottom + 2f, blockGlowPaint)
// Draw block
blockPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE
blockPaint.alpha = if (isGhost) 30 else 255
canvas.drawRect(left, top, right, bottom, blockPaint)
// Draw inner glow
glowPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE
canvas.drawRect(left + 1f, top + 1f, right - 1f, bottom - 1f, glowPaint)
}
"block_skin_2" -> { // Neon
// Stronger outer glow for neon skin
blockGlowPaint.color = if (isGhost) Color.argb(30, 255, 0, 255) else Color.parseColor("#FF00FF")
blockGlowPaint.maskFilter = BlurMaskFilter(16f, BlurMaskFilter.Blur.OUTER)
canvas.drawRect(left - 4f, top - 4f, right + 4f, bottom + 4f, blockGlowPaint)
// For neon, use semi-translucent fill with strong glowing edges
blockPaint.style = Paint.Style.FILL_AND_STROKE
blockPaint.strokeWidth = 2f
blockPaint.maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.NORMAL)
if (isGhost) {
blockPaint.color = Color.argb(30, 255, 0, 255)
blockPaint.alpha = 30
} else {
blockPaint.color = Color.parseColor("#66004D") // Darker magenta fill
blockPaint.alpha = 170 // More opaque to be more visible
}
// Draw block with neon effect
canvas.drawRect(left, top, right, bottom, blockPaint)
// Draw a brighter border for better visibility
val borderPaint = Paint().apply {
color = Color.parseColor("#FF00FF")
style = Paint.Style.STROKE
strokeWidth = 3f
alpha = 255
isAntiAlias = true
maskFilter = BlurMaskFilter(6f, BlurMaskFilter.Blur.NORMAL)
}
canvas.drawRect(left, top, right, bottom, borderPaint)
// Inner glow for neon blocks
glowPaint.color = if (isGhost) Color.argb(10, 255, 0, 255) else Color.parseColor("#FF00FF")
glowPaint.alpha = if (isGhost) 10 else 100
glowPaint.style = Paint.Style.STROKE
glowPaint.strokeWidth = 2f
glowPaint.maskFilter = BlurMaskFilter(4f, BlurMaskFilter.Blur.NORMAL)
canvas.drawRect(left + 4f, top + 4f, right - 4f, bottom - 4f, glowPaint)
}
"block_skin_3" -> { // Retro
// Draw pixelated block with retro effect
blockPaint.color = if (isGhost) Color.argb(30, 255, 90, 95) else Color.parseColor("#FF5A5F")
blockPaint.alpha = if (isGhost) 30 else 255
// Draw main block
canvas.drawRect(left, top, right, bottom, blockPaint)
// Draw pixelated highlights
val highlightPaint = Paint().apply {
color = Color.parseColor("#FF8A8F")
isAntiAlias = false
style = Paint.Style.FILL
}
// Top and left highlights
canvas.drawRect(left, top, right - 2f, top + 2f, highlightPaint)
canvas.drawRect(left, top, left + 2f, bottom - 2f, highlightPaint)
// Draw pixelated shadows
val shadowPaint = Paint().apply {
color = Color.parseColor("#CC4A4F")
isAntiAlias = false
style = Paint.Style.FILL
}
// Bottom and right shadows
canvas.drawRect(left + 2f, bottom - 2f, right, bottom, shadowPaint)
canvas.drawRect(right - 2f, top + 2f, right, bottom - 2f, shadowPaint)
}
"block_skin_4" -> { // Minimalist
// Draw clean, simple block with subtle border
blockPaint.color = if (isGhost) Color.argb(30, 0, 0, 0) else Color.BLACK
blockPaint.alpha = if (isGhost) 30 else 255
blockPaint.style = Paint.Style.FILL
canvas.drawRect(left, top, right, bottom, blockPaint)
// Draw subtle border
val borderPaint = Paint().apply {
color = Color.parseColor("#333333")
style = Paint.Style.STROKE
strokeWidth = 1f
isAntiAlias = true
}
canvas.drawRect(left, top, right, bottom, borderPaint)
}
"block_skin_5" -> { // Galaxy
// Draw cosmic glow effect
blockGlowPaint.color = if (isGhost) Color.argb(30, 102, 252, 241) else Color.parseColor("#66FCF1")
blockGlowPaint.maskFilter = BlurMaskFilter(20f, BlurMaskFilter.Blur.OUTER)
canvas.drawRect(left - 8f, top - 8f, right + 8f, bottom + 8f, blockGlowPaint)
// Draw main block with gradient
val gradient = LinearGradient(
left, top, right, bottom,
Color.parseColor("#66FCF1"),
Color.parseColor("#45B7AF"),
Shader.TileMode.CLAMP
)
blockPaint.shader = gradient
blockPaint.color = if (isGhost) Color.argb(30, 102, 252, 241) else Color.parseColor("#66FCF1")
blockPaint.alpha = if (isGhost) 30 else 255
blockPaint.style = Paint.Style.FILL
canvas.drawRect(left, top, right, bottom, blockPaint)
// Draw star-like sparkles
if (!isGhost) {
val sparklePaint = Paint().apply {
color = Color.WHITE
style = Paint.Style.FILL
isAntiAlias = true
maskFilter = BlurMaskFilter(4f, BlurMaskFilter.Blur.NORMAL)
}
// Add small white dots for sparkle effect
canvas.drawCircle(left + 4f, top + 4f, 1f, sparklePaint)
canvas.drawCircle(right - 4f, bottom - 4f, 1f, sparklePaint)
blockPaint.alpha = alpha
for (y in 0 until height) {
for (x in 0 until width) {
if (piece.isBlockAt(x, y)) {
val left = offsetX + (piece.x + x) * blockSize
val top = offsetY + (piece.y + y) * blockSize
val right = left + blockSize
val bottom = top + blockSize
// Draw block with subtle glow
blockGlowPaint.alpha = alpha / 4
canvas.drawRect(left, top, right, bottom, blockGlowPaint)
// Draw the main block
canvas.drawRect(left + 1f, top + 1f, right - 1f, bottom - 1f, blockPaint)
// Draw border glow
glowPaint.alpha = alpha / 2
canvas.drawRect(left, top, right, bottom, glowPaint)
}
}
}
// Draw pulse effect if animation is active and this is a pulsing line
if (isPulsing && isPulsingLine) {
val pulseBlockPaint = Paint().apply {
color = Color.WHITE
alpha = (255 * pulseAlpha).toInt()
isAntiAlias = true
style = Paint.Style.FILL
maskFilter = BlurMaskFilter(40f * (1f + pulseAlpha), BlurMaskFilter.Blur.OUTER)
}
canvas.drawRect(left - 16f, top - 16f, right + 16f, bottom + 16f, pulseBlockPaint)
}
// Restore canvas state after drawing block effects
canvas.restore()
}
/**
@ -1026,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 -> {
@ -1043,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
}
/**
@ -1182,7 +775,7 @@ class GameView @JvmOverloads constructor(
/**
* Get the next piece that will be spawned
*/
fun getNextPiece(): Tetromino? {
fun getNextPiece(): GamePiece? {
return gameBoard.getNextPiece()
}
@ -1542,4 +1135,103 @@ class GameView @JvmOverloads constructor(
gameHaptics?.vibrateForPieceMove()
invalidate()
}
fun getNextPiece(): GamePiece? = nextPiece
fun getCurrentPiece(): GamePiece? = currentPiece
fun spawnNewPiece() {
currentPiece = gameBoard.spawnNewPiece()
nextPiece = gameBoard.getNextPiece()
onNextPieceChanged?.invoke()
}
fun rotatePiece() {
currentPiece?.let { piece ->
if (gameBoard.rotatePiece(piece)) {
invalidate()
}
}
}
fun movePieceLeft() {
currentPiece?.let { piece ->
if (gameBoard.movePiece(piece, -1, 0)) {
invalidate()
}
}
}
fun movePieceRight() {
currentPiece?.let { piece ->
if (gameBoard.movePiece(piece, 1, 0)) {
invalidate()
}
}
}
fun dropPiece() {
currentPiece?.let { piece ->
if (gameBoard.movePiece(piece, 0, 1)) {
invalidate()
}
}
}
fun hardDrop() {
currentPiece?.let { piece ->
val ghostY = calculateGhostY(piece)
if (ghostY > piece.y) {
piece.y = ghostY
gameBoard.lockPiece(piece)
spawnNewPiece()
invalidate()
}
}
}
fun startGame() {
if (!isRunning) {
isRunning = true
isPaused = false
gameBoard.reset()
spawnNewPiece()
handler.post(gameLoopRunnable)
}
}
fun pauseGame() {
if (isRunning && !isPaused) {
isPaused = true
handler.removeCallbacks(gameLoopRunnable)
}
}
fun resumeGame() {
if (isRunning && isPaused) {
isPaused = false
handler.post(gameLoopRunnable)
}
}
fun stopGame() {
isRunning = false
isPaused = false
handler.removeCallbacks(gameLoopRunnable)
}
fun resetGame() {
stopGame()
gameBoard.reset()
currentPiece = null
nextPiece = null
score = 0
invalidate()
}
fun getScore(): Int = score
fun setGameHaptics(haptics: GameHaptics?) {
gameHaptics = haptics
}
}

View file

@ -1,4 +1,4 @@
package com.mintris.game
package com.pixelmintdrop.game
import android.os.SystemClock
import android.view.InputDevice
@ -8,13 +8,11 @@ import android.util.Log
import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.view.InputDevice.MotionRange
import android.os.Vibrator
import android.os.Handler
import android.os.Looper
/**
* GamepadController handles gamepad input for the Mintris game.
* GamepadController handles gamepad input for the pixelmintdrop game.
* Supports multiple gamepad types including:
* - Microsoft Xbox controllers
* - Sony PlayStation controllers
@ -258,7 +256,7 @@ class GamepadController(
1 -> 100
2 -> 150
3 -> 200
else -> 255 // For tetris (4 lines)
else -> 255 // For quad (4 lines)
}
vibrateGamepad(RUMBLE_LINE_CLEAR_DURATION_MS, amplitude)
}

View file

@ -1,4 +1,4 @@
package com.mintris.game
package com.pixelmintdrop.game
import android.content.Context
import android.graphics.BlurMaskFilter
@ -7,8 +7,7 @@ import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import com.mintris.model.GameBoard
import com.mintris.model.Tetromino
import com.pixelmintdrop.model.GameBoard
import kotlin.math.min
/**

View file

@ -0,0 +1,97 @@
package com.pixelmintdrop.game
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.graphics.BlurMaskFilter
import android.util.AttributeSet
import android.view.View
import kotlin.math.min
/**
* Custom view to display the next game piece
*/
class NextPieceView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var gameView: GameView? = null
// Rendering
private val blockPaint = Paint().apply {
color = Color.WHITE
isAntiAlias = true
}
private val glowPaint = Paint().apply {
color = Color.WHITE
alpha = 30
isAntiAlias = true
style = Paint.Style.STROKE
strokeWidth = 1.5f
}
/**
* Set the game view to get the next piece from
*/
fun setGameView(gameView: GameView) {
this.gameView = gameView
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Get the next piece from game view
gameView?.getNextPiece()?.let { piece ->
val width = piece.getWidth()
val height = piece.getHeight()
// Calculate block size for the preview (smaller than main board)
val previewBlockSize = min(
canvas.width.toFloat() / (width.toFloat() + 2f),
canvas.height.toFloat() / (height.toFloat() + 2f)
)
// Center the piece in the preview area
val previewLeft = (canvas.width.toFloat() - width.toFloat() * previewBlockSize) / 2f
val previewTop = (canvas.height.toFloat() - height.toFloat() * previewBlockSize) / 2f
// Draw subtle background glow
val glowPaint = Paint().apply {
color = Color.WHITE
alpha = 10
maskFilter = BlurMaskFilter(previewBlockSize * 0.5f, BlurMaskFilter.Blur.OUTER)
}
canvas.drawRect(
previewLeft - previewBlockSize,
previewTop - previewBlockSize,
previewLeft + width.toFloat() * previewBlockSize + previewBlockSize,
previewTop + height.toFloat() * previewBlockSize + previewBlockSize,
glowPaint
)
for (y in 0 until height) {
for (x in 0 until width) {
if (piece.isBlockAt(x, y)) {
val left = previewLeft + x.toFloat() * previewBlockSize
val top = previewTop + y.toFloat() * previewBlockSize
val right = left + previewBlockSize
val bottom = top + previewBlockSize
// Draw block with subtle glow
val rect = RectF(left + 1f, top + 1f, right - 1f, bottom - 1f)
canvas.drawRect(rect, blockPaint)
// Draw subtle border glow
val glowRect = RectF(left, top, right, bottom)
canvas.drawRect(glowRect, glowPaint)
}
}
}
}
}
}

View file

@ -1,4 +1,4 @@
package com.mintris.game
package com.pixelmintdrop.game
import android.content.Context
import android.graphics.Canvas
@ -10,9 +10,9 @@ import android.view.MotionEvent
import android.view.View
import java.util.Random
import android.util.Log
import com.mintris.model.HighScoreManager
import com.mintris.model.HighScore
import com.mintris.model.PlayerProgressionManager
import com.pixelmintdrop.model.HighScoreManager
import com.pixelmintdrop.model.HighScore
import com.pixelmintdrop.model.PlayerProgressionManager
import kotlin.math.abs
import androidx.core.graphics.withTranslation
import androidx.core.graphics.withScale
@ -33,7 +33,7 @@ class TitleScreen @JvmOverloads constructor(
private val random = Random()
private var width = 0
private var height = 0
private val tetrominosToAdd = mutableListOf<Tetromino>()
private val piecesToAdd = mutableListOf<FallingPiece>()
private val highScoreManager = HighScoreManager(context) // Pre-allocate HighScoreManager
// Touch handling variables
@ -50,8 +50,8 @@ class TitleScreen @JvmOverloads constructor(
private var themeColor = Color.WHITE
private var backgroundColor = Color.BLACK
// Define tetromino shapes (I, O, T, S, Z, J, L)
private val tetrominoShapes = arrayOf(
// Define piece shapes (I, O, T, S, Z, J, L)
private val pieceShapes = arrayOf(
// I
arrayOf(
intArrayOf(0, 0, 0, 0),
@ -96,8 +96,8 @@ class TitleScreen @JvmOverloads constructor(
)
)
// Tetromino class to represent falling pieces
private class Tetromino(
// FallingPiece class to represent falling pieces
private class FallingPiece(
var x: Float,
var y: Float,
val shape: Array<IntArray>,
@ -106,7 +106,7 @@ class TitleScreen @JvmOverloads constructor(
val rotation: Int = 0
)
private val tetrominos = mutableListOf<Tetromino>()
private val pieces = mutableListOf<FallingPiece>()
init {
// Title text settings
@ -138,14 +138,14 @@ class TitleScreen @JvmOverloads constructor(
alpha = 200
}
// General paint settings for tetrominos
// General paint settings for pieces
paint.apply {
color = themeColor
style = Paint.Style.FILL
isAntiAlias = true
}
// Glow paint settings for tetrominos
// Glow paint settings for pieces
glowPaint.apply {
color = themeColor
style = Paint.Style.FILL
@ -159,26 +159,26 @@ class TitleScreen @JvmOverloads constructor(
width = w
height = h
// Clear existing tetrominos
tetrominos.clear()
// Clear existing pieces
pieces.clear()
// Initialize some tetrominos
// Initialize some pieces
repeat(20) {
val tetromino = createRandomTetromino()
tetrominos.add(tetromino)
val piece = createRandomPiece()
pieces.add(piece)
}
}
private fun createRandomTetromino(): Tetromino {
private fun createRandomPiece(): FallingPiece {
val x = random.nextFloat() * (width - 150) + 50 // Keep away from edges
val y = -cellSize * 4 - (random.nextFloat() * height / 2)
val shapeIndex = random.nextInt(tetrominoShapes.size)
val shape = tetrominoShapes[shapeIndex]
val shapeIndex = random.nextInt(pieceShapes.size)
val shape = pieceShapes[shapeIndex]
val speed = 1f + random.nextFloat() * 2f
val scale = 0.8f + random.nextFloat() * 0.4f
val rotation = random.nextInt(4) * 90
return Tetromino(x, y, shape, speed, scale, rotation)
return FallingPiece(x, y, shape, speed, scale, rotation)
}
override fun onDraw(canvas: Canvas) {
@ -188,37 +188,37 @@ class TitleScreen @JvmOverloads constructor(
// Draw background using the current background color
canvas.drawColor(backgroundColor)
// Add any pending tetrominos
tetrominos.addAll(tetrominosToAdd)
tetrominosToAdd.clear()
// Add any pending pieces
pieces.addAll(piecesToAdd)
piecesToAdd.clear()
// Update and draw falling tetrominos
val tetrominosToRemove = mutableListOf<Tetromino>()
// Update and draw falling pieces
val piecesToRemove = mutableListOf<FallingPiece>()
for (tetromino in tetrominos) {
tetromino.y += tetromino.speed
for (piece in pieces) {
piece.y += piece.speed
// Remove tetrominos that have fallen off the screen
if (tetromino.y > height) {
tetrominosToRemove.add(tetromino)
tetrominosToAdd.add(createRandomTetromino())
// Remove pieces that have fallen off the screen
if (piece.y > height) {
piecesToRemove.add(piece)
piecesToAdd.add(createRandomPiece())
} else {
try {
// Draw the tetromino
for (y in 0 until tetromino.shape.size) {
for (x in 0 until tetromino.shape.size) {
if (tetromino.shape[y][x] == 1) {
// Draw the piece
for (y in 0 until piece.shape.size) {
for (x in 0 until piece.shape.size) {
if (piece.shape[y][x] == 1) {
val left = x * cellSize
val top = y * cellSize
val right = left + cellSize
val bottom = top + cellSize
// Draw block with glow effect
canvas.withTranslation(tetromino.x, tetromino.y) {
withScale(tetromino.scale, tetromino.scale) {
withRotation(tetromino.rotation.toFloat(),
tetromino.shape.size * cellSize / 2,
tetromino.shape.size * cellSize / 2) {
canvas.withTranslation(piece.x, piece.y) {
withScale(piece.scale, piece.scale) {
withRotation(piece.rotation.toFloat(),
piece.shape.size * cellSize / 2,
piece.shape.size * cellSize / 2) {
// Draw glow
canvas.drawRect(left - 8f, top - 8f, right + 8f, bottom + 8f, glowPaint)
// Draw block
@ -230,17 +230,17 @@ class TitleScreen @JvmOverloads constructor(
}
}
} catch (e: Exception) {
Log.e("TitleScreen", "Error drawing tetromino", e)
Log.e("TitleScreen", "Error drawing piece", e)
}
}
}
// Remove tetrominos that fell off the screen
tetrominos.removeAll(tetrominosToRemove)
// Remove pieces that fell off the screen
pieces.removeAll(piecesToRemove)
// Draw title
val titleY = height * 0.4f
canvas.drawText("mintris", width / 2f, titleY, titlePaint)
canvas.drawText("Pixel Mint Drop", width / 2f, titleY, titlePaint)
// Draw high scores using pre-allocated manager
val highScores: List<HighScore> = highScoreManager.getHighScores()
@ -292,10 +292,10 @@ class TitleScreen @JvmOverloads constructor(
val deltaX = event.x - lastTouchX
val deltaY = event.y - lastTouchY
// Update tetromino positions
for (tetromino in tetrominos) {
tetromino.x += deltaX * 0.5f
tetromino.y += deltaY * 0.5f
// Update piece positions
for (piece in pieces) {
piece.x += deltaX * 0.5f
piece.y += deltaY * 0.5f
}
lastTouchX = event.x

View file

@ -0,0 +1,199 @@
package com.pixelmintdrop.model
import android.util.Log
/**
* Represents the game board and manages piece placement and collision detection
*/
class GameBoard {
companion object {
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 state
private val board = Array(HEIGHT) { Array(WIDTH) { false } }
private var currentPiece: GamePiece? = null
private var nextPiece: GamePiece? = null
// Game state
var dropInterval = INITIAL_DROP_INTERVAL
private set
val width: Int = WIDTH
val height: Int = HEIGHT
fun reset() {
// Clear the board
for (y in 0 until HEIGHT) {
for (x in 0 until WIDTH) {
board[y][x] = false
}
}
// Reset pieces
currentPiece = null
nextPiece = null
// Reset drop interval
dropInterval = INITIAL_DROP_INTERVAL
}
fun spawnNewPiece(): GamePiece {
// Get the next piece or create a new one if none exists
currentPiece = nextPiece ?: createRandomPiece()
nextPiece = createRandomPiece()
// Position the piece at the top center of the board
currentPiece?.let { piece ->
piece.x = (WIDTH - piece.getWidth()) / 2
piece.y = 0
}
return currentPiece!!
}
fun getNextPiece(): GamePiece? = nextPiece
fun getCurrentPiece(): GamePiece? = currentPiece
fun hasBlockAt(x: Int, y: Int): Boolean {
if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) return false
return board[y][x]
}
fun wouldCollide(piece: GamePiece, newX: Int, newY: Int): Boolean {
val width = piece.getWidth()
val height = piece.getHeight()
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 board boundaries
if (boardX < 0 || boardX >= WIDTH || boardY >= HEIGHT) {
return true
}
// 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
}
fun lockPiece(piece: GamePiece) {
val width = piece.getWidth()
val height = piece.getHeight()
// 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
if (boardY >= 0 && boardY < HEIGHT && boardX >= 0 && boardX < WIDTH) {
board[boardY][boardX] = true
}
}
}
}
// Clear any completed lines
clearLines()
}
private fun clearLines() {
var linesCleared = 0
var y = HEIGHT - 1
while (y >= 0) {
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 line
for (x in 0 until WIDTH) {
board[0][x] = false
}
linesCleared++
} else {
y--
}
}
// Increase speed based on lines cleared
if (linesCleared > 0) {
dropInterval = (dropInterval * 0.95).toLong().coerceAtLeast(MIN_DROP_INTERVAL)
}
}
private fun isLineFull(y: Int): Boolean {
for (x in 0 until WIDTH) {
if (!board[y][x]) return false
}
return true
}
private fun createRandomPiece(): GamePiece {
return GamePiece(GamePieceType.values().random())
}
}

View file

@ -0,0 +1,48 @@
package com.pixelmintdrop.model
/**
* Represents a game piece with its type, position, and rotation
*/
class GamePiece(val type: GamePieceType) {
var x: Int = 0
var y: Int = 0
var rotation: Int = 0
private set
private val blocks: Array<Array<Boolean>> = type.getBlocks()
fun getWidth(): Int = blocks[0].size
fun getHeight(): Int = blocks.size
fun isBlockAt(x: Int, y: Int): Boolean {
if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight()) return false
return blocks[y][x]
}
fun rotate() {
rotation = (rotation + 1) % 4
rotateBlocks()
}
private fun rotateBlocks() {
val width = getWidth()
val height = getHeight()
val rotated = Array(width) { Array(height) { false } }
for (y in 0 until height) {
for (x in 0 until width) {
when (rotation) {
1 -> rotated[x][height - 1 - y] = blocks[y][x]
2 -> rotated[height - 1 - y][width - 1 - x] = blocks[y][x]
3 -> rotated[width - 1 - x][y] = blocks[y][x]
else -> rotated[y][x] = blocks[y][x]
}
}
}
blocks.indices.forEach { i ->
blocks[i] = rotated[i].copyOf()
}
}
}

View file

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

View file

@ -1,4 +1,4 @@
package com.mintris.model
package com.pixelmintdrop.model
data class HighScore(
val name: String,

View file

@ -1,4 +1,4 @@
package com.mintris.model
package com.pixelmintdrop.model
import android.graphics.Color
import android.view.LayoutInflater
@ -6,7 +6,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.mintris.R
import com.pixelmintdrop.R
class HighScoreAdapter : RecyclerView.Adapter<HighScoreAdapter.HighScoreViewHolder>() {
private var highScores: List<HighScore> = emptyList()

View file

@ -1,4 +1,4 @@
package com.mintris.model
package com.pixelmintdrop.model
import android.content.Context
import android.content.SharedPreferences
@ -12,7 +12,7 @@ class HighScoreManager(private val context: Context) {
private val type: Type = object : TypeToken<List<HighScore>>() {}.type
companion object {
private const val PREFS_NAME = "mintris_highscores"
private const val PREFS_NAME = "pixelmintdrop_highscores"
private const val KEY_HIGHSCORES = "highscores"
private const val MAX_HIGHSCORES = 5
}

View file

@ -1,8 +1,7 @@
package com.mintris.model
package com.pixelmintdrop.model
import android.content.Context
import android.content.SharedPreferences
import com.mintris.R
import kotlin.math.pow
import kotlin.math.roundToInt
import kotlin.math.min
@ -94,24 +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, 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()
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
// 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()
}
/**
@ -292,7 +298,7 @@ class PlayerProgressionManager(context: Context) {
}
companion object {
private const val PREFS_NAME = "mintris_progression"
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"
@ -328,6 +334,12 @@ class PlayerProgressionManager(context: Context) {
THEME_MINIMALIST to 20,
THEME_GALAXY to 25
)
private const val BASE_SCORE_XP = 0.1 // XP per score point
private const val BASE_LINES_XP = 100.0 // XP per line cleared
private const val BASE_QUAD_BONUS = 500.0 // Bonus XP for clearing 4 lines at once
private const val BASE_PERFECT_CLEAR_BONUS = 1000.0 // Bonus XP for perfect clear
private const val BASE_TIME_XP = 1.0 // XP per second played
}
/**

View file

@ -0,0 +1,78 @@
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)
// Session stats
private var sessionScore = 0
private var sessionLines = 0
private var sessionQuads = 0
private var sessionPerfectClears = 0
fun getHighScore(): Int = prefs.getInt(KEY_HIGH_SCORE, 0)
fun getTotalGames(): Int = prefs.getInt(KEY_TOTAL_GAMES, 0)
fun getTotalLines(): Int = prefs.getInt(KEY_TOTAL_LINES, 0)
fun getTotalQuads(): Int = prefs.getInt(KEY_TOTAL_QUADS, 0)
fun getTotalPerfectClears(): Int = prefs.getInt(KEY_TOTAL_PERFECT_CLEARS, 0)
fun getSessionScore(): Int = sessionScore
fun getSessionLines(): Int = sessionLines
fun getSessionQuads(): Int = sessionQuads
fun getSessionPerfectClears(): Int = sessionPerfectClears
fun updateStats(score: Int, lines: Int, isQuad: Boolean, isPerfectClear: Boolean) {
// Update session stats
sessionScore += score
sessionLines += lines
if (isQuad) sessionQuads++
if (isPerfectClear) sessionPerfectClears++
// Update high score if needed
if (sessionScore > getHighScore()) {
prefs.edit().putInt(KEY_HIGH_SCORE, sessionScore).apply()
}
// Update total stats
prefs.edit()
.putInt(KEY_TOTAL_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 startNewGame() {
// Increment total games counter
prefs.edit().putInt(KEY_TOTAL_GAMES, getTotalGames() + 1).apply()
}
fun resetSession() {
sessionScore = 0
sessionLines = 0
sessionQuads = 0
sessionPerfectClears = 0
}
fun resetAllStats() {
prefs.edit().clear().apply()
resetSession()
}
}

View file

@ -1,7 +1,7 @@
package com.mintris.model
package com.pixelmintdrop.model
/**
* Represents a Tetris piece (Tetromino)
* Represents a game piece (Tetromino)
*/
enum class TetrominoType {
I, J, L, O, S, T, Z

View file

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

View file

@ -1,4 +1,4 @@
package com.mintris.ui
package com.pixelmintdrop.ui
import android.content.Context
import android.graphics.Color
@ -9,9 +9,7 @@ import android.widget.FrameLayout
import android.widget.GridLayout
import android.widget.TextView
import androidx.cardview.widget.CardView
import com.mintris.R
import com.mintris.model.PlayerProgressionManager
import android.animation.ValueAnimator
import com.pixelmintdrop.R
import android.graphics.drawable.GradientDrawable
/**

View file

@ -1,14 +1,10 @@
package com.mintris.ui
package com.pixelmintdrop.ui
import android.content.Context
import android.graphics.Color
import android.view.View
import android.widget.ImageButton
import android.widget.TextView
import com.mintris.R
import com.mintris.databinding.ActivityMainBinding
import com.mintris.model.PlayerProgressionManager
import kotlin.math.roundToInt
import com.pixelmintdrop.databinding.ActivityMainBinding
import com.pixelmintdrop.model.PlayerProgressionManager
/**
* Handles UI updates and state management for the game interface

View file

@ -1,4 +1,4 @@
package com.mintris.ui
package com.pixelmintdrop.ui
import android.content.Context
import android.graphics.Canvas

View file

@ -1,4 +1,4 @@
package com.mintris.ui
package com.pixelmintdrop.ui
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
@ -12,8 +12,8 @@ import android.view.animation.OvershootInterpolator
import android.widget.LinearLayout
import android.widget.TextView
import androidx.cardview.widget.CardView
import com.mintris.R
import com.mintris.model.PlayerProgressionManager
import com.pixelmintdrop.R
import com.pixelmintdrop.model.PlayerProgressionManager
/**
* Screen that displays player progression, XP gain, and unlocked rewards

View file

@ -1,4 +1,4 @@
package com.mintris.ui
package com.pixelmintdrop.ui
import android.content.Context
import android.graphics.Color
@ -9,9 +9,8 @@ import android.widget.FrameLayout
import android.widget.GridLayout
import android.widget.TextView
import androidx.cardview.widget.CardView
import com.mintris.R
import com.mintris.model.PlayerProgressionManager
import android.animation.ValueAnimator
import com.pixelmintdrop.R
import com.pixelmintdrop.model.PlayerProgressionManager
import android.graphics.drawable.GradientDrawable
import android.util.Log

View file

@ -1,4 +1,4 @@
package com.mintris.ui
package com.pixelmintdrop.ui
import android.animation.ValueAnimator
import android.content.Context
@ -9,8 +9,6 @@ import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import androidx.core.content.ContextCompat
import com.mintris.R
/**
* Custom progress bar for displaying player XP with animation capabilities

View file

@ -5,7 +5,7 @@
android:viewportWidth="108"
android:viewportHeight="108">
<!-- T-Tetromino -->
<!-- T-piece -->
<path
android:fillColor="#FFFFFF"
android:pathData="M36,36h12v12h-12z" />
@ -19,7 +19,7 @@
android:fillColor="#FFFFFF"
android:pathData="M48,48h12v12h-12z" />
<!-- L-Tetromino -->
<!-- L-piece -->
<path
android:fillColor="#FFFFFF"
android:pathData="M36,60h12v12h-12z" />

View file

@ -28,7 +28,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.mintris.game.GameView
<com.pixelmintdrop.game.GameView
android:id="@+id/gameView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
@ -72,7 +72,7 @@
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="24dp"/>
<com.mintris.game.HoldPieceView
<com.pixelmintdrop.game.HoldPieceView
android:id="@+id/holdPieceView"
android:layout_width="80dp"
android:layout_height="80dp"
@ -125,7 +125,7 @@
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="24dp"/>
<com.mintris.game.NextPieceView
<com.pixelmintdrop.game.NextPieceView
android:id="@+id/nextPieceView"
android:layout_width="80dp"
android:layout_height="80dp"
@ -188,7 +188,7 @@
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- Title Screen -->
<com.mintris.game.TitleScreen
<com.pixelmintdrop.game.TitleScreen
android:id="@+id/titleScreen"
android:layout_width="0dp"
android:layout_height="0dp"
@ -198,7 +198,7 @@
app:layout_constraintTop_toTopOf="parent" />
<!-- Progression Screen -->
<com.mintris.ui.ProgressionScreen
<com.pixelmintdrop.ui.ProgressionScreen
android:id="@+id/progressionScreen"
android:layout_width="0dp"
android:layout_height="0dp"
@ -209,7 +209,7 @@
app:layout_constraintTop_toTopOf="parent" />
<!-- Theme Selector -->
<com.mintris.ui.ThemeSelector
<com.pixelmintdrop.ui.ThemeSelector
android:id="@+id/themeSelector"
android:layout_width="0dp"
android:layout_height="0dp"
@ -220,7 +220,7 @@
app:layout_constraintTop_toTopOf="parent" />
<!-- Block Skin Selector -->
<com.mintris.ui.BlockSkinSelector
<com.pixelmintdrop.ui.BlockSkinSelector
android:id="@+id/blockSkinSelector"
android:layout_width="0dp"
android:layout_height="0dp"
@ -592,7 +592,7 @@
android:fontFamily="sans-serif" />
<TextView
android:id="@+id/sessionTetrisesText"
android:id="@+id/sessionQuadsText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
@ -655,7 +655,7 @@
android:textAllCaps="false"
android:singleLine="true" />
<com.mintris.ui.LevelBadge
<com.pixelmintdrop.ui.LevelBadge
android:id="@+id/customizationLevelBadge"
android:layout_width="48dp"
android:layout_height="48dp" />
@ -683,7 +683,7 @@
android:paddingBottom="32dp">
<!-- Theme Selector -->
<com.mintris.ui.ThemeSelector
<com.pixelmintdrop.ui.ThemeSelector
android:id="@+id/customizationThemeSelector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -691,7 +691,7 @@
android:focusableInTouchMode="true" />
<!-- Block Skin Selector -->
<com.mintris.ui.BlockSkinSelector
<com.pixelmintdrop.ui.BlockSkinSelector
android:id="@+id/customizationBlockSkinSelector"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View file

@ -117,7 +117,7 @@
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/totalTetrisesText"
android:id="@+id/totalQuadsText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"

View file

@ -19,7 +19,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.mintris.game.GameView
<com.pixelmintdrop.game.GameView
android:id="@+id/gameView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
@ -33,7 +33,7 @@
</FrameLayout>
<!-- Title Screen -->
<com.mintris.game.TitleScreen
<com.pixelmintdrop.game.TitleScreen
android:id="@+id/titleScreen"
android:layout_width="0dp"
android:layout_height="0dp"
@ -116,7 +116,7 @@
android:fontFamily="sans-serif"
android:layout_marginBottom="4dp" />
<com.mintris.game.NextPieceView
<com.pixelmintdrop.game.NextPieceView
android:id="@+id/nextPieceView"
android:layout_width="80dp"
android:layout_height="80dp" />
@ -136,7 +136,7 @@
</LinearLayout>
<!-- Hold Piece Preview -->
<com.mintris.game.HoldPieceView
<com.pixelmintdrop.game.HoldPieceView
android:id="@+id/holdPieceView"
android:layout_width="60dp"
android:layout_height="60dp"
@ -266,7 +266,7 @@
android:fontFamily="sans-serif" />
<TextView
android:id="@+id/sessionTetrisesText"
android:id="@+id/sessionQuadsText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
@ -289,7 +289,7 @@
</LinearLayout>
<!-- Player Progression Screen -->
<com.mintris.ui.ProgressionScreen
<com.pixelmintdrop.ui.ProgressionScreen
android:id="@+id/progressionScreen"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -328,7 +328,7 @@
android:layout_marginEnd="16dp"
android:textAllCaps="false" />
<com.mintris.ui.LevelBadge
<com.pixelmintdrop.ui.LevelBadge
android:id="@+id/pauseLevelBadge"
android:layout_width="48dp"
android:layout_height="48dp" />
@ -567,7 +567,7 @@
android:textAllCaps="false"
android:singleLine="true" />
<com.mintris.ui.LevelBadge
<com.pixelmintdrop.ui.LevelBadge
android:id="@+id/customizationLevelBadge"
android:layout_width="48dp"
android:layout_height="48dp" />
@ -592,14 +592,14 @@
android:paddingBottom="32dp">
<!-- Theme Selector -->
<com.mintris.ui.ThemeSelector
<com.pixelmintdrop.ui.ThemeSelector
android:id="@+id/customizationThemeSelector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp" />
<!-- Block Skin Selector -->
<com.mintris.ui.BlockSkinSelector
<com.pixelmintdrop.ui.BlockSkinSelector
android:id="@+id/customizationBlockSkinSelector"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View file

@ -101,7 +101,7 @@
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/totalTetrisesText"
android:id="@+id/totalQuadsText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"

View file

@ -30,7 +30,7 @@
android:gravity="center"
android:layout_marginBottom="24dp" />
<com.mintris.ui.XPProgressBar
<com.pixelmintdrop.ui.XPProgressBar
android:id="@+id/xp_progress_bar"
android:layout_width="match_parent"
android:layout_height="50dp"

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">mintris</string>
<string name="app_name">Pixel Mint Drop</string>
<string name="game_over">game over</string>
<string name="score">score</string>
<string name="level">level</string>
@ -44,7 +44,7 @@
<string name="singles">singles: %d</string>
<string name="doubles">doubles: %d</string>
<string name="triples">triples: %d</string>
<string name="tetrises">tetrises: %d</string>
<string name="quads">quads: %d</string>
<string name="reset_stats">reset stats</string>
<string name="music">music</string>
<string name="customization">Customization</string>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme -->
<style name="Theme.Mintris" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<style name="Theme.pixelmintdrop" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="colorPrimary">@color/white</item>
<item name="colorPrimaryDark">@color/black</item>
<item name="colorAccent">@color/white</item>
@ -11,7 +11,7 @@
</style>
<!-- No action bar theme -->
<style name="Theme.Mintris.NoActionBar">
<style name="Theme.pixelmintdrop.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowFullscreen">true</item>

View file

@ -8,8 +8,8 @@
android:shortcutLongLabel="@string/shortcut_new_game_long_label">
<intent
android:action="android.intent.action.VIEW"
android:targetPackage="com.mintris"
android:targetClass="com.mintris.MainActivity">
android:targetPackage="com.pixelmintdrop"
android:targetClass="com.pixelmintdrop.MainActivity">
<extra android:name="action" android:value="new_game" />
</intent>
<categories android:name="android.shortcut.conversation" />
@ -23,8 +23,8 @@
android:shortcutLongLabel="@string/shortcut_high_scores_long_label">
<intent
android:action="android.intent.action.VIEW"
android:targetPackage="com.mintris"
android:targetClass="com.mintris.HighScoresActivity" />
android:targetPackage="com.pixelmintdrop"
android:targetClass="com.pixelmintdrop.HighScoresActivity" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
</shortcuts>

View file

@ -1,2 +1,2 @@
rootProject.name = "Mintris"
rootProject.name = "pixelmintdrop"
include ':app'

12
tatus
View file

@ -1,8 +1,8 @@
app/src/main/java/com/mintris/MainActivity.kt
app/src/main/java/com/mintris/audio/GameMusic.kt
app/src/main/java/com/mintris/model/PlayerProgressionManager.kt
app/src/main/java/com/mintris/ui/ProgressionScreen.kt
app/src/main/java/com/mintris/ui/ThemeSelector.kt
app/src/main/java/com/mintris/ui/XPProgressBar.kt
app/src/main/java/com/pixelmintdrop/MainActivity.kt
app/src/main/java/com/pixelmintdrop/audio/GameMusic.kt
app/src/main/java/com/pixelmintdrop/model/PlayerProgressionManager.kt
app/src/main/java/com/pixelmintdrop/ui/ProgressionScreen.kt
app/src/main/java/com/pixelmintdrop/ui/ThemeSelector.kt
app/src/main/java/com/pixelmintdrop/ui/XPProgressBar.kt
app/src/main/res/drawable/rounded_button.xml
app/src/main/res/layout/progression_screen.xml