diff --git a/app/build.gradle b/app/build.gradle
index c36d139..da55975 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -49,6 +49,7 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0'
implementation 'androidx.window:window:1.2.0' // For better display support
implementation 'androidx.window:window-java:1.2.0'
+ implementation 'com.google.code.gson:gson:2.10.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 69eb6bc..bc5761a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -20,5 +20,15 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/mintris/HighScoreEntryActivity.kt b/app/src/main/java/com/mintris/HighScoreEntryActivity.kt
new file mode 100644
index 0000000..ef2e13a
--- /dev/null
+++ b/app/src/main/java/com/mintris/HighScoreEntryActivity.kt
@@ -0,0 +1,39 @@
+package com.mintris
+
+import android.os.Bundle
+import android.widget.Button
+import android.widget.EditText
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+import com.mintris.model.HighScore
+import com.mintris.model.HighScoreManager
+
+class HighScoreEntryActivity : AppCompatActivity() {
+ private lateinit var highScoreManager: HighScoreManager
+ private lateinit var nameInput: EditText
+ private lateinit var scoreText: TextView
+ private lateinit var saveButton: Button
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.high_score_entry)
+
+ highScoreManager = HighScoreManager(this)
+ nameInput = findViewById(R.id.nameInput)
+ scoreText = findViewById(R.id.scoreText)
+ saveButton = findViewById(R.id.saveButton)
+
+ val score = intent.getIntExtra("score", 0)
+ val level = intent.getIntExtra("level", 1)
+ scoreText.text = getString(R.string.score) + ": $score"
+
+ saveButton.setOnClickListener {
+ val name = nameInput.text.toString().trim()
+ if (name.isNotEmpty()) {
+ val highScore = HighScore(name, score, level)
+ highScoreManager.addHighScore(highScore)
+ finish()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/mintris/HighScoresActivity.kt b/app/src/main/java/com/mintris/HighScoresActivity.kt
new file mode 100644
index 0000000..da6dd9c
--- /dev/null
+++ b/app/src/main/java/com/mintris/HighScoresActivity.kt
@@ -0,0 +1,45 @@
+package com.mintris
+
+import android.os.Bundle
+import android.widget.Button
+import androidx.appcompat.app.AppCompatActivity
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.mintris.model.HighScoreAdapter
+import com.mintris.model.HighScoreManager
+
+class HighScoresActivity : AppCompatActivity() {
+ private lateinit var highScoreManager: HighScoreManager
+ private lateinit var highScoreAdapter: HighScoreAdapter
+ private lateinit var highScoresList: RecyclerView
+ private lateinit var backButton: Button
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.high_scores)
+
+ highScoreManager = HighScoreManager(this)
+ highScoresList = findViewById(R.id.highScoresList)
+ backButton = findViewById(R.id.backButton)
+
+ highScoreAdapter = HighScoreAdapter()
+ highScoresList.layoutManager = LinearLayoutManager(this)
+ highScoresList.adapter = highScoreAdapter
+
+ updateHighScores()
+
+ backButton.setOnClickListener {
+ finish()
+ }
+ }
+
+ private fun updateHighScores() {
+ val scores = highScoreManager.getHighScores()
+ highScoreAdapter.updateHighScores(scores)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ updateHighScores()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/mintris/MainActivity.kt b/app/src/main/java/com/mintris/MainActivity.kt
index 5d0c01b..205484d 100644
--- a/app/src/main/java/com/mintris/MainActivity.kt
+++ b/app/src/main/java/com/mintris/MainActivity.kt
@@ -19,6 +19,8 @@ import com.mintris.game.TitleScreen
import android.view.HapticFeedbackConstants
import com.mintris.model.GameBoard
import com.mintris.audio.GameMusic
+import com.mintris.model.HighScoreManager
+import android.content.Intent
class MainActivity : AppCompatActivity() {
@@ -29,12 +31,15 @@ class MainActivity : AppCompatActivity() {
private lateinit var gameBoard: GameBoard
private lateinit var gameMusic: GameMusic
private lateinit var titleScreen: TitleScreen
+ private lateinit var highScoreManager: HighScoreManager
// Game state
private var isSoundEnabled = true
private var isMusicEnabled = true
private var selectedLevel = 1
private val maxLevel = 20
+ private var currentScore = 0
+ private var currentLevel = 1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -47,6 +52,7 @@ class MainActivity : AppCompatActivity() {
gameView = binding.gameView
titleScreen = binding.titleScreen
gameMusic = GameMusic(this)
+ highScoreManager = HighScoreManager(this)
// Set up game view
gameView.setGameBoard(gameBoard)
@@ -151,6 +157,11 @@ class MainActivity : AppCompatActivity() {
startGame()
}
+ binding.highScoresButton.setOnClickListener {
+ gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
+ showHighScores()
+ }
+
binding.pauseLevelUpButton.setOnClickListener {
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
if (selectedLevel < maxLevel) {
@@ -193,6 +204,16 @@ class MainActivity : AppCompatActivity() {
*/
private fun showGameOver(score: Int) {
binding.finalScoreText.text = getString(R.string.score) + ": " + score
+
+ // Check if this is a high score
+ if (highScoreManager.isHighScore(score)) {
+ val intent = Intent(this, HighScoreEntryActivity::class.java).apply {
+ putExtra("score", score)
+ putExtra("level", currentLevel)
+ }
+ startActivity(intent)
+ }
+
binding.gameOverContainer.visibility = View.VISIBLE
// Vibrate to indicate game over
@@ -314,4 +335,12 @@ class MainActivity : AppCompatActivity() {
binding.pauseContainer.visibility = View.GONE
titleScreen.visibility = View.VISIBLE
}
+
+ /**
+ * Show high scores
+ */
+ private fun showHighScores() {
+ val intent = Intent(this, HighScoresActivity::class.java)
+ startActivity(intent)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/mintris/game/GameView.kt b/app/src/main/java/com/mintris/game/GameView.kt
index e82baaf..388c45b 100644
--- a/app/src/main/java/com/mintris/game/GameView.kt
+++ b/app/src/main/java/com/mintris/game/GameView.kt
@@ -206,25 +206,8 @@ class GameView @JvmOverloads constructor(
// Move the current tetromino down automatically
gameBoard.moveDown()
- // Check if lines need to be cleared
- if (gameBoard.linesToClear.isNotEmpty()) {
- // Trigger line clear callback for vibration
- onLineClear?.invoke(gameBoard.linesToClear.size)
-
- // Trigger line clearing on a background thread to prevent UI freezes
- Thread {
- // Process the line clearing off the UI thread
- gameBoard.clearLinesFromGrid()
-
- // Then update UI on the main thread
- handler.post {
- invalidate()
- }
- }.start()
- } else {
- // Update UI with current game state
- onGameStateChanged?.invoke(gameBoard.score, gameBoard.level, gameBoard.lines)
- }
+ // Update UI with current game state
+ onGameStateChanged?.invoke(gameBoard.score, gameBoard.level, gameBoard.lines)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
diff --git a/app/src/main/java/com/mintris/model/GameBoard.kt b/app/src/main/java/com/mintris/model/GameBoard.kt
index 5302ba2..a8db083 100644
--- a/app/src/main/java/com/mintris/model/GameBoard.kt
+++ b/app/src/main/java/com/mintris/model/GameBoard.kt
@@ -265,174 +265,135 @@ class GameBoard(
// Trigger the piece lock vibration
onPieceLock?.invoke()
- // Find lines to clear
- findLinesToClear()
+ // Find and clear lines immediately
+ findAndClearLines()
- // Only spawn a new piece if we're not in the middle of clearing lines
- if (!isLineClearAnimationInProgress) {
- spawnPiece()
- }
+ // Spawn new piece
+ spawnPiece()
// Allow holding piece again after locking
canHold = true
}
/**
- * Find lines that should be cleared and store them
+ * Find and clear completed lines immediately
*/
- private fun findLinesToClear() {
- // Clear existing lines
- linesToClear.clear()
-
+ private fun findAndClearLines() {
// Quick scan for completed lines
- for (y in 0 until height) {
- val row = grid[y]
-
- // Check if line is full - use all() for better performance
- if (row.all { it }) {
- linesToClear.add(y)
+ var shiftAmount = 0
+ var y = height - 1
+
+ while (y >= 0) {
+ if (grid[y].all { it }) {
+ // Line is full, increment shift amount
+ shiftAmount++
+ } else if (shiftAmount > 0) {
+ // Shift this row down by shiftAmount
+ System.arraycopy(grid[y], 0, grid[y + shiftAmount], 0, width)
}
+ y--
}
- // Sort lines from bottom to top for proper clearing
- if (linesToClear.isNotEmpty()) {
- linesToClear.sortDescending()
- isLineClearAnimationInProgress = true
+ // Clear top rows
+ for (y in 0 until shiftAmount) {
+ java.util.Arrays.fill(grid[y], false)
+ }
+
+ // If lines were cleared, calculate score in background
+ if (shiftAmount > 0) {
+ Thread {
+ calculateScore(shiftAmount)
+ }.start()
}
}
/**
- * Prepare for line clearing animation
+ * Calculate score for cleared lines
*/
- fun finishLineClearingEffect() {
- if (linesToClear.isNotEmpty() && !isLineClearAnimationInProgress) {
- clearLinesFromGrid()
+ 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
}
- }
-
- /**
- * Actually clear the lines from the grid after animation
- */
- fun clearLinesFromGrid() {
- if (linesToClear.isNotEmpty()) {
- val clearedLines = linesToClear.size
-
- // Much faster approach: shift rows in-place without creating temporary arrays
- // Pre-compute all row movements to minimize array operations
- val rowMoves = IntArray(height) { -1 } // Where each row should move to
- var shiftAmount = 0
-
- // Calculate how much to shift each row
- for (y in height - 1 downTo 0) {
- if (y in linesToClear) {
- shiftAmount++
- } else if (shiftAmount > 0) {
- rowMoves[y] = y + shiftAmount
- }
+
+ // 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
}
-
- // Apply row shifts in a single pass, bottom to top
- for (y in height - 1 downTo 0) {
- val targetY = rowMoves[y]
- if (targetY != -1 && targetY < height) {
- // Shift this row down
- System.arraycopy(grid[y], 0, grid[targetY], 0, width)
- }
+ } 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
}
-
- // Clear top rows (faster than creating a new array)
- for (y in 0 until clearedLines) {
- java.util.Arrays.fill(grid[y], false)
+ } 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
}
-
- // Calculate base score (NES scoring system)
- val baseScore = when (clearedLines) {
- 1 -> 40
- 2 -> 100
- 3 -> 300
- 4 -> 1200 // Tetris
- 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()
-
+ } 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
-
- // Update combo counter
- if (clearedLines > 0) {
- combo++
- } else {
- combo = 0
- }
-
- // Update line clear state
- lastClearWasTetris = clearedLines == 4
- lastClearWasPerfect = isPerfectClear
- lastClearWasAllClear = isAllClear
-
- // Update lines cleared and level
- lines += clearedLines
- level = (lines / 10) + 1
-
- // Update game speed based on level (NES formula)
- dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong()
-
- // Reset animation state immediately
- isLineClearAnimationInProgress = false
- linesToClear.clear()
-
- // Now spawn the next piece after all lines are cleared
- spawnPiece()
+ }.start()
+
+ // Update combo counter
+ if (clearedLines > 0) {
+ combo++
+ } else {
+ combo = 0
}
+
+ // Update line clear state
+ lastClearWasTetris = clearedLines == 4
+ lastClearWasPerfect = isPerfectClear
+ lastClearWasAllClear = isAllClear
+
+ // Update lines cleared and level
+ lines += clearedLines
+ level = (lines / 10) + 1
+
+ // Update game speed based on level (NES formula)
+ dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong()
}
/**
diff --git a/app/src/main/java/com/mintris/model/HighScore.kt b/app/src/main/java/com/mintris/model/HighScore.kt
new file mode 100644
index 0000000..b4b6295
--- /dev/null
+++ b/app/src/main/java/com/mintris/model/HighScore.kt
@@ -0,0 +1,8 @@
+package com.mintris.model
+
+data class HighScore(
+ val name: String,
+ val score: Int,
+ val level: Int,
+ val date: Long = System.currentTimeMillis()
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/mintris/model/HighScoreAdapter.kt b/app/src/main/java/com/mintris/model/HighScoreAdapter.kt
new file mode 100644
index 0000000..1f86531
--- /dev/null
+++ b/app/src/main/java/com/mintris/model/HighScoreAdapter.kt
@@ -0,0 +1,42 @@
+package com.mintris.model
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.mintris.R
+
+class HighScoreAdapter : RecyclerView.Adapter() {
+ private var highScores: List = emptyList()
+
+ fun updateHighScores(newHighScores: List) {
+ highScores = newHighScores
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HighScoreViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_high_score, parent, false)
+ return HighScoreViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: HighScoreViewHolder, position: Int) {
+ val highScore = highScores[position]
+ holder.bind(highScore, position + 1)
+ }
+
+ override fun getItemCount(): Int = highScores.size
+
+ class HighScoreViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ private val rankText: TextView = itemView.findViewById(R.id.rankText)
+ private val nameText: TextView = itemView.findViewById(R.id.nameText)
+ private val scoreText: TextView = itemView.findViewById(R.id.scoreText)
+
+ fun bind(highScore: HighScore, rank: Int) {
+ rankText.text = "#$rank"
+ nameText.text = highScore.name
+ scoreText.text = highScore.score.toString()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/mintris/model/HighScoreManager.kt b/app/src/main/java/com/mintris/model/HighScoreManager.kt
new file mode 100644
index 0000000..6fd7df9
--- /dev/null
+++ b/app/src/main/java/com/mintris/model/HighScoreManager.kt
@@ -0,0 +1,47 @@
+package com.mintris.model
+
+import android.content.Context
+import android.content.SharedPreferences
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import java.lang.reflect.Type
+
+class HighScoreManager(private val context: Context) {
+ private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
+ private val gson = Gson()
+ private val type: Type = object : TypeToken>() {}.type
+
+ companion object {
+ private const val PREFS_NAME = "mintris_highscores"
+ private const val KEY_HIGHSCORES = "highscores"
+ private const val MAX_HIGHSCORES = 5
+ }
+
+ fun getHighScores(): List {
+ val json = prefs.getString(KEY_HIGHSCORES, null)
+ return if (json != null) {
+ gson.fromJson(json, type)
+ } else {
+ emptyList()
+ }
+ }
+
+ fun addHighScore(highScore: HighScore) {
+ val currentScores = getHighScores().toMutableList()
+ currentScores.add(highScore)
+
+ // Sort by score (descending) and keep only top 5
+ currentScores.sortByDescending { it.score }
+ val topScores = currentScores.take(MAX_HIGHSCORES)
+
+ // Save to SharedPreferences
+ val json = gson.toJson(topScores)
+ prefs.edit().putString(KEY_HIGHSCORES, json).apply()
+ }
+
+ fun isHighScore(score: Int): Boolean {
+ val currentScores = getHighScores()
+ return currentScores.size < MAX_HIGHSCORES ||
+ score > (currentScores.lastOrNull()?.score ?: 0)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/edit_text_background.xml b/app/src/main/res/drawable/edit_text_background.xml
new file mode 100644
index 0000000..aaeb099
--- /dev/null
+++ b/app/src/main/res/drawable/edit_text_background.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index f18eabe..d83c8aa 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -190,6 +190,16 @@
android:text="@string/restart"
android:textColor="@color/white"
android:textSize="18sp" />
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/high_scores.xml b/app/src/main/res/layout/high_scores.xml
new file mode 100644
index 0000000..bac8f83
--- /dev/null
+++ b/app/src/main/res/layout/high_scores.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_high_score.xml b/app/src/main/res/layout/item_high_score.xml
new file mode 100644
index 0000000..18859c2
--- /dev/null
+++ b/app/src/main/res/layout/item_high_score.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4ac2073..871a7af 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -16,4 +16,8 @@
Sound: On
Sound: Off
Toggle music
+ High Scores
+ New High Score!
+ Save
+ Back
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 7e74683..cd0519b 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -20,7 +20,4 @@ kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
-android.nonTransitiveRClass=true
-
-# Set Java Home to Java 17
-org.gradle.java.home=/opt/homebrew/Cellar/openjdk@17/17.0.14/libexec/openjdk.jdk/Contents/Home
\ No newline at end of file
+android.nonTransitiveRClass=true
\ No newline at end of file