mirror of
https://github.com/cmclark00/mintris.git
synced 2025-05-18 00:05:21 +01:00
Initial commit: Modern Tetris implementation with official rules and scoring system
This commit is contained in:
commit
f4e5a9b651
27 changed files with 2898 additions and 0 deletions
59
.gitignore
vendored
Normal file
59
.gitignore
vendored
Normal file
|
@ -0,0 +1,59 @@
|
|||
# Built application files
|
||||
*.apk
|
||||
*.aar
|
||||
*.ap_
|
||||
*.aab
|
||||
|
||||
# Files for the ART/Dalvik VM
|
||||
*.dex
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# Generated files
|
||||
bin/
|
||||
gen/
|
||||
out/
|
||||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Log Files
|
||||
*.log
|
||||
|
||||
# Android Studio Navigation editor temp files
|
||||
.navigation/
|
||||
|
||||
# Android Studio captures folder
|
||||
captures/
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.idea/
|
||||
|
||||
# External native build folder
|
||||
.externalNativeBuild
|
||||
.cxx/
|
||||
|
||||
# Mac OS
|
||||
.DS_Store
|
||||
|
||||
# Signing files
|
||||
.signing/
|
||||
|
||||
# Version control
|
||||
vcs.xml
|
||||
|
||||
# Keystore files
|
||||
*.jks
|
||||
*.keystore
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
google-services.json
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"java.configuration.updateBuildConfiguration": "automatic"
|
||||
}
|
124
README.md
Normal file
124
README.md
Normal file
|
@ -0,0 +1,124 @@
|
|||
# Mintris
|
||||
|
||||
A modern Tetris implementation for Android, featuring official Tetris rules, smooth animations, and responsive controls.
|
||||
|
||||
## Features
|
||||
|
||||
### Core Gameplay
|
||||
- Official Tetris rules and mechanics
|
||||
- 7-bag randomizer for piece distribution
|
||||
- Hold piece functionality
|
||||
- Ghost piece preview
|
||||
- Hard drop and soft drop
|
||||
- Wall kick system for rotations
|
||||
- T-Spin detection and scoring
|
||||
|
||||
### Modern Android Features
|
||||
- Optimized for Android 10+ (API 29+)
|
||||
- Hardware-accelerated rendering
|
||||
- High refresh rate support
|
||||
- Haptic feedback
|
||||
- Dark theme support
|
||||
- Responsive touch controls
|
||||
|
||||
### Scoring System
|
||||
|
||||
The game features a comprehensive scoring system based on official Tetris rules:
|
||||
|
||||
#### Base Scores
|
||||
- Single line: 40 points
|
||||
- Double: 100 points
|
||||
- Triple: 300 points
|
||||
- Tetris (4 lines): 1200 points
|
||||
|
||||
#### Multipliers
|
||||
|
||||
1. **Level Multiplier**
|
||||
- Score is multiplied by current level
|
||||
- Level increases every 10 lines cleared
|
||||
|
||||
2. **Combo System**
|
||||
- Combo counter increases with each line clear
|
||||
- Resets if no lines are cleared
|
||||
- Multipliers:
|
||||
- 1 combo: 1.0x
|
||||
- 2 combos: 1.5x
|
||||
- 3 combos: 2.0x
|
||||
- 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
|
||||
|
||||
4. **Perfect Clear**
|
||||
- 2x for single line
|
||||
- 3x for double
|
||||
- 4x for triple
|
||||
- 5x for Tetris
|
||||
- Awarded when clearing lines without leaving blocks
|
||||
|
||||
5. **All Clear**
|
||||
- 2x multiplier when clearing all blocks
|
||||
- Requires no blocks in grid and no pieces in play
|
||||
|
||||
6. **T-Spin Bonuses**
|
||||
- Single: 2x
|
||||
- Double: 4x
|
||||
- Triple: 6x
|
||||
|
||||
#### Example Score Calculation
|
||||
A back-to-back T-Spin Tetris with a 3x combo at level 2:
|
||||
```
|
||||
Base Score: 1200
|
||||
Level: 2
|
||||
Combo: 3x
|
||||
Back-to-Back: 1.5x
|
||||
T-Spin: 6x
|
||||
Final Score: 1200 * 2 * 3 * 1.5 * 6 = 64,800
|
||||
```
|
||||
|
||||
### Controls
|
||||
- Tap left/right to move piece
|
||||
- Tap up to rotate
|
||||
- Double tap to hard drop
|
||||
- Long press to hold piece
|
||||
- Swipe down for soft drop
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Requirements
|
||||
- Android 10 (API 29) or higher
|
||||
- OpenGL ES 2.0 or higher
|
||||
- 2GB RAM minimum
|
||||
|
||||
### Performance Optimizations
|
||||
- Hardware-accelerated rendering
|
||||
- Efficient collision detection
|
||||
- Optimized grid operations
|
||||
- Smooth animations at 60fps
|
||||
|
||||
### Architecture
|
||||
- Written in Kotlin
|
||||
- Uses Android Canvas for rendering
|
||||
- Implements MVVM architecture
|
||||
- Follows Material Design guidelines
|
||||
|
||||
## Building from Source
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/yourusername/mintris.git
|
||||
```
|
||||
|
||||
2. Open the project in Android Studio
|
||||
|
||||
3. Build and run on your device or emulator
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
55
app/build.gradle
Normal file
55
app/build.gradle
Normal file
|
@ -0,0 +1,55 @@
|
|||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'com.mintris'
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.mintris"
|
||||
minSdk 30
|
||||
targetSdk 34
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
|
||||
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'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
}
|
17
app/proguard-rules.pro
vendored
Normal file
17
app/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Keep models intact
|
||||
-keep class com.mintris.model.** { *; }
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
24
app/src/main/AndroidManifest.xml
Normal file
24
app/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Mintris">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.Mintris.NoActionBar">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
283
app/src/main/java/com/mintris/MainActivity.kt
Normal file
283
app/src/main/java/com/mintris/MainActivity.kt
Normal file
|
@ -0,0 +1,283 @@
|
|||
package com.mintris
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import android.os.VibratorManager
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.mintris.databinding.ActivityMainBinding
|
||||
import com.mintris.game.GameHaptics
|
||||
import com.mintris.game.GameView
|
||||
import com.mintris.game.NextPieceView
|
||||
import android.view.HapticFeedbackConstants
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
// UI components
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var gameView: GameView
|
||||
private lateinit var scoreText: TextView
|
||||
private lateinit var levelText: TextView
|
||||
private lateinit var linesText: TextView
|
||||
private lateinit var gameOverContainer: LinearLayout
|
||||
private lateinit var pauseContainer: LinearLayout
|
||||
private lateinit var playAgainButton: Button
|
||||
private lateinit var resumeButton: Button
|
||||
private lateinit var settingsButton: Button
|
||||
private lateinit var finalScoreText: TextView
|
||||
private lateinit var nextPieceView: NextPieceView
|
||||
private lateinit var levelDownButton: Button
|
||||
private lateinit var levelUpButton: Button
|
||||
private lateinit var selectedLevelText: TextView
|
||||
private lateinit var gameHaptics: GameHaptics
|
||||
|
||||
// Game state
|
||||
private var isSoundEnabled = true
|
||||
private var selectedLevel = 1
|
||||
private val maxLevel = 10
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
// Initialize game haptics
|
||||
gameHaptics = GameHaptics(this)
|
||||
|
||||
// Set up game view
|
||||
gameView = binding.gameView
|
||||
scoreText = binding.scoreText
|
||||
levelText = binding.levelText
|
||||
linesText = binding.linesText
|
||||
gameOverContainer = binding.gameOverContainer
|
||||
pauseContainer = binding.pauseContainer
|
||||
playAgainButton = binding.playAgainButton
|
||||
resumeButton = binding.resumeButton
|
||||
settingsButton = binding.settingsButton
|
||||
finalScoreText = binding.finalScoreText
|
||||
nextPieceView = binding.nextPieceView
|
||||
levelDownButton = binding.levelDownButton
|
||||
levelUpButton = binding.levelUpButton
|
||||
selectedLevelText = binding.selectedLevelText
|
||||
|
||||
// Connect the next piece view to the game view
|
||||
nextPieceView.setGameView(gameView)
|
||||
|
||||
// Set up callbacks
|
||||
gameView.onGameStateChanged = { score, level, lines ->
|
||||
updateUI(score, level, lines)
|
||||
}
|
||||
|
||||
gameView.onGameOver = { score ->
|
||||
showGameOver(score)
|
||||
}
|
||||
|
||||
gameView.onLineClear = { lineCount ->
|
||||
// Use enhanced haptic feedback for line clears
|
||||
if (isSoundEnabled) {
|
||||
gameHaptics.vibrateForLineClear(lineCount)
|
||||
}
|
||||
}
|
||||
|
||||
// Add callbacks for piece movement and locking
|
||||
gameView.onPieceMove = {
|
||||
if (isSoundEnabled) {
|
||||
gameHaptics.vibrateForPieceMove()
|
||||
}
|
||||
}
|
||||
|
||||
gameView.onPieceLock = {
|
||||
if (isSoundEnabled) {
|
||||
gameHaptics.vibrateForPieceLock()
|
||||
}
|
||||
}
|
||||
|
||||
// Set up button click listeners with haptic feedback
|
||||
playAgainButton.setOnClickListener {
|
||||
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
hideGameOver()
|
||||
gameView.reset()
|
||||
setGameLevel(selectedLevel)
|
||||
gameView.start()
|
||||
}
|
||||
|
||||
resumeButton.setOnClickListener {
|
||||
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
hidePauseMenu()
|
||||
gameView.start()
|
||||
}
|
||||
|
||||
settingsButton.setOnClickListener {
|
||||
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
toggleSound()
|
||||
}
|
||||
|
||||
// Set up level selector with haptic feedback
|
||||
levelDownButton.setOnClickListener {
|
||||
if (selectedLevel > 1) {
|
||||
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
selectedLevel--
|
||||
updateLevelSelector()
|
||||
}
|
||||
}
|
||||
|
||||
levelUpButton.setOnClickListener {
|
||||
if (selectedLevel < maxLevel) {
|
||||
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
selectedLevel++
|
||||
updateLevelSelector()
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize level selector
|
||||
updateLevelSelector()
|
||||
|
||||
// Start game when clicking the screen initially
|
||||
setupTouchToStart()
|
||||
|
||||
// Start with the game paused
|
||||
gameView.pause()
|
||||
|
||||
// Enable edge-to-edge display
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window.setDecorFitsSystemWindows(false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up touch-to-start behavior for initial screen
|
||||
*/
|
||||
private fun setupTouchToStart() {
|
||||
val touchToStart = View.OnClickListener {
|
||||
if (gameView.isGameOver()) {
|
||||
hideGameOver()
|
||||
gameView.reset()
|
||||
gameView.start()
|
||||
} else if (pauseContainer.visibility == View.VISIBLE) {
|
||||
hidePauseMenu()
|
||||
gameView.start()
|
||||
} else {
|
||||
gameView.start()
|
||||
}
|
||||
}
|
||||
|
||||
// Add the click listener to the game view
|
||||
gameView.setOnClickListener(touchToStart)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update UI with current game state
|
||||
*/
|
||||
private fun updateUI(score: Int, level: Int, lines: Int) {
|
||||
scoreText.text = score.toString()
|
||||
levelText.text = level.toString()
|
||||
linesText.text = lines.toString()
|
||||
|
||||
// Force redraw of next piece preview
|
||||
nextPieceView.invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Show game over screen
|
||||
*/
|
||||
private fun showGameOver(score: Int) {
|
||||
finalScoreText.text = getString(R.string.score) + ": " + score
|
||||
gameOverContainer.visibility = View.VISIBLE
|
||||
|
||||
// Vibrate to indicate game over
|
||||
vibrate(VibrationEffect.EFFECT_DOUBLE_CLICK)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide game over screen
|
||||
*/
|
||||
private fun hideGameOver() {
|
||||
gameOverContainer.visibility = View.GONE
|
||||
}
|
||||
|
||||
/**
|
||||
* Show pause menu
|
||||
*/
|
||||
private fun showPauseMenu() {
|
||||
pauseContainer.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide pause menu
|
||||
*/
|
||||
private fun hidePauseMenu() {
|
||||
pauseContainer.visibility = View.GONE
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle sound on/off
|
||||
*/
|
||||
private fun toggleSound() {
|
||||
isSoundEnabled = !isSoundEnabled
|
||||
settingsButton.text = getString(
|
||||
if (isSoundEnabled) R.string.sound_on else R.string.sound_off
|
||||
)
|
||||
|
||||
// Vibrate to provide feedback
|
||||
vibrate(VibrationEffect.EFFECT_CLICK)
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger device vibration with predefined effect
|
||||
*/
|
||||
private fun vibrate(effectId: Int) {
|
||||
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
vibrator.vibrate(VibrationEffect.createPredefined(effectId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the level selector display
|
||||
*/
|
||||
private fun updateLevelSelector() {
|
||||
selectedLevelText.text = selectedLevel.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the game level
|
||||
*/
|
||||
private fun setGameLevel(level: Int) {
|
||||
gameView.gameBoard.level = level
|
||||
gameView.gameBoard.lines = (level - 1) * 10
|
||||
gameView.gameBoard.dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong()
|
||||
|
||||
// Update UI
|
||||
levelText.text = level.toString()
|
||||
linesText.text = gameView.gameBoard.lines.toString()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (!gameView.isGameOver()) {
|
||||
gameView.pause()
|
||||
showPauseMenu()
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onBackPressed() {
|
||||
if (gameOverContainer.visibility == View.VISIBLE) {
|
||||
hideGameOver()
|
||||
gameView.reset()
|
||||
return
|
||||
}
|
||||
|
||||
if (pauseContainer.visibility == View.GONE) {
|
||||
gameView.pause()
|
||||
showPauseMenu()
|
||||
} else {
|
||||
hidePauseMenu()
|
||||
gameView.start()
|
||||
}
|
||||
}
|
||||
}
|
68
app/src/main/java/com/mintris/game/GameHaptics.kt
Normal file
68
app/src/main/java/com/mintris/game/GameHaptics.kt
Normal file
|
@ -0,0 +1,68 @@
|
|||
package com.mintris.game
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import android.os.VibratorManager
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.View
|
||||
|
||||
class GameHaptics(private val context: Context) {
|
||||
private val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
|
||||
vibratorManager.defaultVibrator
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
}
|
||||
|
||||
fun performHapticFeedback(view: View, feedbackType: Int) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.CONFIRM)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
view.performHapticFeedback(feedbackType)
|
||||
}
|
||||
}
|
||||
|
||||
fun vibrateForLineClear(lineCount: Int) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val duration = when (lineCount) {
|
||||
4 -> 100L // Tetris
|
||||
3 -> 80L
|
||||
2 -> 60L
|
||||
1 -> 40L
|
||||
else -> 0L
|
||||
}
|
||||
|
||||
val amplitude = when (lineCount) {
|
||||
4 -> VibrationEffect.DEFAULT_AMPLITUDE
|
||||
3 -> (VibrationEffect.DEFAULT_AMPLITUDE * 0.8).toInt()
|
||||
2 -> (VibrationEffect.DEFAULT_AMPLITUDE * 0.6).toInt()
|
||||
1 -> (VibrationEffect.DEFAULT_AMPLITUDE * 0.4).toInt()
|
||||
else -> 0
|
||||
}
|
||||
|
||||
if (duration > 0 && amplitude > 0) {
|
||||
val vibrationEffect = VibrationEffect.createOneShot(duration, amplitude)
|
||||
vibrator.vibrate(vibrationEffect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun vibrateForPieceLock() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val vibrationEffect = VibrationEffect.createOneShot(50L, VibrationEffect.DEFAULT_AMPLITUDE)
|
||||
vibrator.vibrate(vibrationEffect)
|
||||
}
|
||||
}
|
||||
|
||||
fun vibrateForPieceMove() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val amplitude = (VibrationEffect.DEFAULT_AMPLITUDE * 0.3).toInt().coerceAtLeast(1)
|
||||
val vibrationEffect = VibrationEffect.createOneShot(20L, amplitude)
|
||||
vibrator.vibrate(vibrationEffect)
|
||||
}
|
||||
}
|
||||
}
|
620
app/src/main/java/com/mintris/game/GameView.kt
Normal file
620
app/src/main/java/com/mintris/game/GameView.kt
Normal file
|
@ -0,0 +1,620 @@
|
|||
package com.mintris.game
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.animation.LinearInterpolator
|
||||
import android.view.WindowManager
|
||||
import android.view.Display
|
||||
import android.hardware.display.DisplayManager
|
||||
import com.mintris.model.GameBoard
|
||||
import com.mintris.model.Tetromino
|
||||
import com.mintris.model.TetrominoType
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* GameView that renders the Tetris game and handles touch input
|
||||
*/
|
||||
class GameView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
// Game board model
|
||||
val gameBoard = GameBoard()
|
||||
|
||||
// Game state
|
||||
private var isRunning = false
|
||||
private var isPaused = false
|
||||
|
||||
// Rendering
|
||||
private val blockPaint = Paint().apply {
|
||||
color = Color.WHITE
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
private val ghostBlockPaint = Paint().apply {
|
||||
color = Color.WHITE
|
||||
alpha = 80 // 30% opacity
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
private val gridPaint = Paint().apply {
|
||||
color = Color.parseColor("#222222") // Very dark gray
|
||||
alpha = 40
|
||||
isAntiAlias = true
|
||||
strokeWidth = 1f
|
||||
style = Paint.Style.STROKE
|
||||
}
|
||||
|
||||
private val glowPaint = Paint().apply {
|
||||
color = Color.WHITE
|
||||
alpha = 80
|
||||
isAntiAlias = true
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 2f
|
||||
}
|
||||
|
||||
private val borderGlowPaint = Paint().apply {
|
||||
color = Color.CYAN
|
||||
alpha = 120
|
||||
isAntiAlias = true
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 4f
|
||||
setShadowLayer(10f, 0f, 0f, Color.CYAN)
|
||||
}
|
||||
|
||||
private val lineClearPaint = Paint().apply {
|
||||
color = Color.WHITE
|
||||
alpha = 255
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
// Animation
|
||||
private var lineClearAnimator: ValueAnimator? = null
|
||||
private var lineClearProgress = 0f
|
||||
private val lineClearDuration = 150L // milliseconds
|
||||
|
||||
// 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() {
|
||||
if (isRunning && !isPaused) {
|
||||
update()
|
||||
invalidate()
|
||||
handler.postDelayed(this, gameBoard.dropInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Touch parameters
|
||||
private var lastTouchX = 0f
|
||||
private var lastTouchY = 0f
|
||||
private var startX = 0f
|
||||
private var startY = 0f
|
||||
private var lastTapTime = 0L
|
||||
private var lastRotationTime = 0L
|
||||
private var minSwipeVelocity = 800 // Minimum velocity for swipe to be considered a hard drop
|
||||
private val maxTapMovement = 20f // Maximum movement allowed for a tap (in pixels)
|
||||
private val minTapTime = 100L // Minimum time for a tap (in milliseconds)
|
||||
private val rotationCooldown = 150L // Minimum time between rotations (in milliseconds)
|
||||
|
||||
// Callback for game events
|
||||
var onGameStateChanged: ((score: Int, level: Int, lines: Int) -> Unit)? = null
|
||||
var onGameOver: ((score: Int) -> Unit)? = null
|
||||
var onLineClear: ((Int) -> Unit)? = null // New callback for line clear events
|
||||
var onPieceMove: (() -> Unit)? = null // New callback for piece movement
|
||||
var onPieceLock: (() -> Unit)? = null // New callback for piece locking
|
||||
|
||||
init {
|
||||
// Start with paused state
|
||||
pause()
|
||||
|
||||
// Connect our callbacks to the GameBoard
|
||||
gameBoard.onPieceMove = { onPieceMove?.invoke() }
|
||||
gameBoard.onPieceLock = { onPieceLock?.invoke() }
|
||||
|
||||
// Enable hardware acceleration
|
||||
setLayerType(LAYER_TYPE_HARDWARE, null)
|
||||
|
||||
// Set better frame rate using modern APIs
|
||||
val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
|
||||
val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)
|
||||
display?.let { disp ->
|
||||
val refreshRate = disp.refreshRate
|
||||
// Set game loop interval based on refresh rate, but don't go faster than the base interval
|
||||
val targetFps = refreshRate.toInt()
|
||||
if (targetFps > 0) {
|
||||
gameBoard.dropInterval = gameBoard.dropInterval.coerceAtMost(1000L / targetFps)
|
||||
}
|
||||
}
|
||||
|
||||
// Enable edge-to-edge rendering
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
setSystemGestureExclusionRects(listOf(Rect(0, 0, width, height)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the game
|
||||
*/
|
||||
fun start() {
|
||||
if (isPaused || !isRunning) {
|
||||
isPaused = false
|
||||
if (!isRunning) {
|
||||
isRunning = true
|
||||
gameBoard.reset()
|
||||
}
|
||||
handler.post(gameLoopRunnable)
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause the game
|
||||
*/
|
||||
fun pause() {
|
||||
isPaused = true
|
||||
handler.removeCallbacks(gameLoopRunnable)
|
||||
lineClearAnimator?.cancel()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the game
|
||||
*/
|
||||
fun reset() {
|
||||
isRunning = false
|
||||
isPaused = true
|
||||
gameBoard.reset()
|
||||
handler.removeCallbacks(gameLoopRunnable)
|
||||
lineClearAnimator?.cancel()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update game state (called on game loop)
|
||||
*/
|
||||
private fun update() {
|
||||
if (gameBoard.isGameOver) {
|
||||
isRunning = false
|
||||
isPaused = true
|
||||
onGameOver?.invoke(gameBoard.score)
|
||||
return
|
||||
}
|
||||
|
||||
// Move the current tetromino down automatically
|
||||
gameBoard.moveDown()
|
||||
|
||||
// Check if lines need to be cleared and start animation if needed
|
||||
if (gameBoard.linesToClear.isNotEmpty() && gameBoard.isLineClearAnimationInProgress) {
|
||||
// Trigger line clear callback for vibration
|
||||
onLineClear?.invoke(gameBoard.linesToClear.size)
|
||||
|
||||
// Start line clearing animation if not already running
|
||||
if (lineClearAnimator == null || !lineClearAnimator!!.isRunning) {
|
||||
startLineClearAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
// Update UI with current game state
|
||||
onGameStateChanged?.invoke(gameBoard.score, gameBoard.level, gameBoard.lines)
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the line clearing animation
|
||||
*/
|
||||
private fun startLineClearAnimation() {
|
||||
lineClearAnimator?.cancel()
|
||||
|
||||
lineClearAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
|
||||
duration = lineClearDuration
|
||||
interpolator = LinearInterpolator()
|
||||
|
||||
addUpdateListener { animator ->
|
||||
lineClearProgress = animator.animatedValue as Float
|
||||
invalidate()
|
||||
}
|
||||
|
||||
addListener(object : android.animation.AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: android.animation.Animator) {
|
||||
// When animation completes, actually clear the lines
|
||||
gameBoard.clearLinesFromGrid()
|
||||
invalidate()
|
||||
}
|
||||
})
|
||||
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
|
||||
// Enable hardware acceleration
|
||||
setLayerType(LAYER_TYPE_HARDWARE, null)
|
||||
|
||||
// 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
|
||||
|
||||
// Calculate block size to fit within the view
|
||||
blockSize = min(
|
||||
width.toFloat() / horizontalBlocks,
|
||||
height.toFloat() / verticalBlocks
|
||||
)
|
||||
|
||||
// Center the board within the view
|
||||
boardLeft = (width - (blockSize * horizontalBlocks)) / 2
|
||||
boardTop = (height - (blockSize * verticalBlocks)) / 2
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
// Draw background (already black from theme)
|
||||
|
||||
// Draw board border glow
|
||||
drawBoardBorder(canvas)
|
||||
|
||||
// Draw grid (very subtle)
|
||||
drawGrid(canvas)
|
||||
|
||||
// Check if line clear animation is in progress
|
||||
if (gameBoard.isLineClearAnimationInProgress) {
|
||||
// Draw the line clearing animation
|
||||
drawLineClearAnimation(canvas)
|
||||
} else {
|
||||
// Draw locked pieces
|
||||
drawLockedBlocks(canvas)
|
||||
}
|
||||
|
||||
if (!gameBoard.isGameOver && isRunning) {
|
||||
// Draw ghost piece (landing preview)
|
||||
drawGhostPiece(canvas)
|
||||
|
||||
// Draw active piece
|
||||
drawActivePiece(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the line clearing animation
|
||||
*/
|
||||
private fun drawLineClearAnimation(canvas: Canvas) {
|
||||
// Draw non-clearing blocks
|
||||
for (y in 0 until gameBoard.height) {
|
||||
// Skip lines that are being cleared
|
||||
if (gameBoard.linesToClear.contains(y)) continue
|
||||
|
||||
for (x in 0 until gameBoard.width) {
|
||||
if (gameBoard.isOccupied(x, y)) {
|
||||
drawBlock(canvas, x, y, blockPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw all clearing lines with a single animation effect
|
||||
for (lineY in gameBoard.linesToClear) {
|
||||
for (x in 0 until gameBoard.width) {
|
||||
// Animation effects for all lines simultaneously
|
||||
val brightness = 255 - (lineClearProgress * 200).toInt()
|
||||
val scale = 1.0f - lineClearProgress * 0.5f
|
||||
|
||||
// Set the paint for the clear animation
|
||||
lineClearPaint.color = Color.WHITE
|
||||
lineClearPaint.alpha = brightness.coerceIn(0, 255)
|
||||
|
||||
// Calculate block position with scaling
|
||||
val left = boardLeft + x * blockSize + (blockSize * (1 - scale) / 2)
|
||||
val top = boardTop + lineY * blockSize + (blockSize * (1 - scale) / 2)
|
||||
val right = left + blockSize * scale
|
||||
val bottom = top + blockSize * scale
|
||||
|
||||
// Draw the shrinking, fading block
|
||||
val rect = RectF(left, top, right, bottom)
|
||||
canvas.drawRect(rect, lineClearPaint)
|
||||
|
||||
// Add a glow effect
|
||||
lineClearPaint.setShadowLayer(10f * (1f - lineClearProgress), 0f, 0f, Color.WHITE)
|
||||
canvas.drawRect(rect, lineClearPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw glowing border around the playable area
|
||||
*/
|
||||
private fun drawBoardBorder(canvas: Canvas) {
|
||||
val left = boardLeft
|
||||
val top = boardTop
|
||||
val right = boardLeft + gameBoard.width * blockSize
|
||||
val bottom = boardTop + gameBoard.height * blockSize
|
||||
|
||||
val rect = RectF(left, top, right, bottom)
|
||||
canvas.drawRect(rect, borderGlowPaint)
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the grid lines (very subtle)
|
||||
*/
|
||||
private fun drawGrid(canvas: Canvas) {
|
||||
// Draw vertical grid lines
|
||||
for (x in 0..gameBoard.width) {
|
||||
val xPos = boardLeft + x * blockSize
|
||||
canvas.drawLine(
|
||||
xPos, boardTop,
|
||||
xPos, boardTop + gameBoard.height * blockSize,
|
||||
gridPaint
|
||||
)
|
||||
}
|
||||
|
||||
// Draw horizontal grid lines
|
||||
for (y in 0..gameBoard.height) {
|
||||
val yPos = boardTop + y * blockSize
|
||||
canvas.drawLine(
|
||||
boardLeft, yPos,
|
||||
boardLeft + gameBoard.width * blockSize, yPos,
|
||||
gridPaint
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the locked blocks on the board
|
||||
*/
|
||||
private fun drawLockedBlocks(canvas: Canvas) {
|
||||
for (y in 0 until gameBoard.height) {
|
||||
for (x in 0 until gameBoard.width) {
|
||||
if (gameBoard.isOccupied(x, y)) {
|
||||
drawBlock(canvas, x, y, blockPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the currently active tetromino
|
||||
*/
|
||||
private fun drawActivePiece(canvas: Canvas) {
|
||||
val piece = gameBoard.getCurrentPiece() ?: return
|
||||
|
||||
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 draw if within bounds and visible on screen
|
||||
if (boardY >= 0 && boardY < gameBoard.height &&
|
||||
boardX >= 0 && boardX < gameBoard.width) {
|
||||
drawBlock(canvas, boardX, boardY, blockPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the ghost piece (landing preview)
|
||||
*/
|
||||
private fun drawGhostPiece(canvas: Canvas) {
|
||||
val piece = gameBoard.getCurrentPiece() ?: return
|
||||
val ghostY = gameBoard.getGhostY()
|
||||
|
||||
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
|
||||
|
||||
// Only draw if within bounds and visible on screen
|
||||
if (boardY >= 0 && boardY < gameBoard.height &&
|
||||
boardX >= 0 && boardX < gameBoard.width) {
|
||||
drawBlock(canvas, boardX, boardY, ghostBlockPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a single tetris block at the given grid position
|
||||
*/
|
||||
private fun drawBlock(canvas: Canvas, x: Int, y: Int, paint: Paint) {
|
||||
val left = boardLeft + x * blockSize
|
||||
val top = boardTop + y * blockSize
|
||||
val right = left + blockSize
|
||||
val bottom = top + blockSize
|
||||
|
||||
// Draw block with a slight inset to create separation
|
||||
val rect = RectF(left + 1, top + 1, right - 1, bottom - 1)
|
||||
canvas.drawRect(rect, paint)
|
||||
|
||||
// Draw enhanced glow effect
|
||||
val glowRect = RectF(left, top, right, bottom)
|
||||
val blockGlowPaint = Paint(glowPaint)
|
||||
if (paint == blockPaint) {
|
||||
val piece = gameBoard.getCurrentPiece()
|
||||
if (piece != null && isPositionInPiece(x, y, piece)) {
|
||||
// Set glow color based on piece type
|
||||
blockGlowPaint.color = getTetrominoColor(piece.type)
|
||||
blockGlowPaint.alpha = 150
|
||||
blockGlowPaint.setShadowLayer(3f, 0f, 0f, blockGlowPaint.color)
|
||||
}
|
||||
}
|
||||
canvas.drawRect(glowRect, blockGlowPaint)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given board position is part of the current piece
|
||||
*/
|
||||
private fun isPositionInPiece(boardX: Int, boardY: Int, piece: Tetromino): Boolean {
|
||||
for (y in 0 until piece.getHeight()) {
|
||||
for (x in 0 until piece.getWidth()) {
|
||||
if (piece.isBlockAt(x, y)) {
|
||||
val pieceX = piece.x + x
|
||||
val pieceY = piece.y + y
|
||||
if (pieceX == boardX && pieceY == boardY) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color for tetromino type
|
||||
*/
|
||||
private fun getTetrominoColor(type: TetrominoType): Int {
|
||||
return when (type) {
|
||||
TetrominoType.I -> Color.CYAN
|
||||
TetrominoType.J -> Color.BLUE
|
||||
TetrominoType.L -> Color.rgb(255, 165, 0) // Orange
|
||||
TetrominoType.O -> Color.YELLOW
|
||||
TetrominoType.S -> Color.GREEN
|
||||
TetrominoType.T -> Color.MAGENTA
|
||||
TetrominoType.Z -> Color.RED
|
||||
}
|
||||
}
|
||||
|
||||
// Custom touch event handling
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
if (!isRunning || isPaused || gameBoard.isGameOver) {
|
||||
return true
|
||||
}
|
||||
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
// Record start of touch
|
||||
startX = event.x
|
||||
startY = event.y
|
||||
lastTouchX = event.x
|
||||
lastTouchY = event.y
|
||||
|
||||
// Check for double tap (rotate)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (currentTime - lastTapTime < 250) {
|
||||
// Double tap detected, rotate the piece
|
||||
if (currentTime - lastRotationTime >= rotationCooldown) {
|
||||
gameBoard.rotate()
|
||||
lastRotationTime = currentTime
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
lastTapTime = currentTime
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
val deltaX = event.x - lastTouchX
|
||||
val deltaY = event.y - lastTouchY
|
||||
|
||||
// Horizontal movement (left/right)
|
||||
if (abs(deltaX) > blockSize) {
|
||||
if (deltaX > 0) {
|
||||
gameBoard.moveRight()
|
||||
} else {
|
||||
gameBoard.moveLeft()
|
||||
}
|
||||
lastTouchX = event.x
|
||||
invalidate()
|
||||
}
|
||||
|
||||
// Vertical movement (soft drop)
|
||||
if (deltaY > blockSize / 2) {
|
||||
gameBoard.moveDown()
|
||||
lastTouchY = event.y
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
// Calculate movement speed for potential fling detection
|
||||
val moveTime = System.currentTimeMillis() - lastTapTime
|
||||
val deltaY = event.y - startY
|
||||
val deltaX = event.x - startX
|
||||
|
||||
// If the movement was fast and downward, treat as hard drop
|
||||
if (moveTime > 0 && deltaY > blockSize && (deltaY / moveTime) * 1000 > minSwipeVelocity) {
|
||||
gameBoard.hardDrop()
|
||||
invalidate()
|
||||
} else if (moveTime < minTapTime &&
|
||||
abs(deltaY) < maxTapMovement &&
|
||||
abs(deltaX) < maxTapMovement) {
|
||||
// Quick tap with minimal movement (rotation)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (currentTime - lastRotationTime >= rotationCooldown) {
|
||||
gameBoard.rotate()
|
||||
lastRotationTime = currentTime
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current score
|
||||
*/
|
||||
fun getScore(): Int = gameBoard.score
|
||||
|
||||
/**
|
||||
* Get the current level
|
||||
*/
|
||||
fun getLevel(): Int = gameBoard.level
|
||||
|
||||
/**
|
||||
* Get the number of lines cleared
|
||||
*/
|
||||
fun getLines(): Int = gameBoard.lines
|
||||
|
||||
/**
|
||||
* Check if the game is over
|
||||
*/
|
||||
fun isGameOver(): Boolean = gameBoard.isGameOver
|
||||
|
||||
/**
|
||||
* Get the next tetromino
|
||||
*/
|
||||
fun getNextPiece() = gameBoard.getNextPiece()
|
||||
|
||||
/**
|
||||
* Clean up resources when view is detached
|
||||
*/
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
handler.removeCallbacks(gameLoopRunnable)
|
||||
}
|
||||
}
|
82
app/src/main/java/com/mintris/game/NextPieceView.kt
Normal file
82
app/src/main/java/com/mintris/game/NextPieceView.kt
Normal file
|
@ -0,0 +1,82 @@
|
|||
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.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 = 80
|
||||
isAntiAlias = true
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 2f
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
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
|
||||
|
||||
val rect = RectF(left + 1, top + 1, right - 1, bottom - 1)
|
||||
canvas.drawRect(rect, blockPaint)
|
||||
|
||||
val glowRect = RectF(left, top, right, bottom)
|
||||
canvas.drawRect(glowRect, glowPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
534
app/src/main/java/com/mintris/model/GameBoard.kt
Normal file
534
app/src/main/java/com/mintris/model/GameBoard.kt
Normal file
|
@ -0,0 +1,534 @@
|
|||
package com.mintris.model
|
||||
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* Represents the game board (grid) and manages game state
|
||||
*/
|
||||
class GameBoard(
|
||||
val width: Int = 10,
|
||||
val height: Int = 20
|
||||
) {
|
||||
// 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 lines = 0
|
||||
var isGameOver = false
|
||||
|
||||
// Scoring state
|
||||
private var combo = 0
|
||||
private var lastClearWasTetris = false
|
||||
private var lastClearWasPerfect = false
|
||||
private var lastClearWasAllClear = false
|
||||
|
||||
// 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
|
||||
|
||||
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.values())
|
||||
bag.shuffle()
|
||||
}
|
||||
|
||||
// Take the next piece from the bag
|
||||
nextPiece = Tetromino(bag.removeFirst())
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
spawnNextPiece()
|
||||
spawnPiece()
|
||||
} 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
|
||||
|
||||
/**
|
||||
* Spawns the current tetromino at the top of the board
|
||||
*/
|
||||
fun spawnPiece() {
|
||||
currentPiece = nextPiece
|
||||
spawnNextPiece()
|
||||
|
||||
// Center the piece horizontally
|
||||
currentPiece?.apply {
|
||||
x = (width - getWidth()) / 2
|
||||
y = 0
|
||||
|
||||
// Check if the piece can be placed (Game Over condition)
|
||||
if (!canMove(0, 0)) {
|
||||
isGameOver = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
return if (canMove(0, 1)) {
|
||||
currentPiece?.y = currentPiece?.y?.plus(1) ?: 0
|
||||
onPieceMove?.invoke()
|
||||
true
|
||||
} else {
|
||||
lockPiece()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hard drop the current piece
|
||||
*/
|
||||
fun hardDrop() {
|
||||
while (moveDown()) {
|
||||
// Keep moving down until blocked
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
if (boardX < 0 || boardX >= width || 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock the current piece in place and check for completed lines
|
||||
*/
|
||||
fun lockPiece() {
|
||||
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()
|
||||
|
||||
// Find lines to clear
|
||||
findLinesToClear()
|
||||
|
||||
// Only spawn a new piece if we're not in the middle of clearing lines
|
||||
if (!isLineClearAnimationInProgress) {
|
||||
spawnPiece()
|
||||
}
|
||||
|
||||
// Allow holding piece again after locking
|
||||
canHold = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Find lines that should be cleared and store them
|
||||
*/
|
||||
private fun findLinesToClear() {
|
||||
// Clear existing lines before finding new ones
|
||||
linesToClear.clear()
|
||||
|
||||
for (y in 0 until height) {
|
||||
// Check if line is full
|
||||
var lineFull = true
|
||||
for (x in 0 until width) {
|
||||
if (!grid[y][x]) {
|
||||
lineFull = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (lineFull) {
|
||||
linesToClear.add(y)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort lines from bottom to top for proper clearing
|
||||
if (linesToClear.isNotEmpty()) {
|
||||
linesToClear.sortDescending()
|
||||
isLineClearAnimationInProgress = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare for line clearing animation
|
||||
*/
|
||||
fun finishLineClearingEffect() {
|
||||
if (linesToClear.isNotEmpty() && !isLineClearAnimationInProgress) {
|
||||
clearLinesFromGrid()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually clear the lines from the grid after animation
|
||||
*/
|
||||
fun clearLinesFromGrid() {
|
||||
if (linesToClear.isNotEmpty()) {
|
||||
val clearedLines = linesToClear.size
|
||||
|
||||
// Get the highest line that needs to be cleared
|
||||
val highestLine = linesToClear.minOrNull() ?: return
|
||||
|
||||
// Create a temporary grid to store the new state
|
||||
val newGrid = Array(height) { BooleanArray(width) { false } }
|
||||
|
||||
// Copy non-cleared lines to their new positions
|
||||
var newY = height - 1
|
||||
for (y in height - 1 downTo 0) {
|
||||
if (y !in linesToClear) {
|
||||
for (x in 0 until width) {
|
||||
newGrid[newY][x] = grid[y][x]
|
||||
}
|
||||
newY--
|
||||
}
|
||||
}
|
||||
|
||||
// Update the grid with the new state
|
||||
for (y in 0 until height) {
|
||||
for (x in 0 until width) {
|
||||
grid[y][x] = newGrid[y][x]
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
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 and clear lines
|
||||
isLineClearAnimationInProgress = false
|
||||
linesToClear.clear()
|
||||
|
||||
// Now spawn the next piece after all lines are cleared
|
||||
spawnPiece()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
||||
return ghostY
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current tetromino
|
||||
*/
|
||||
fun getCurrentPiece(): Tetromino? = currentPiece
|
||||
|
||||
/**
|
||||
* Get the next tetromino
|
||||
*/
|
||||
fun getNextPiece(): Tetromino? = nextPiece
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = 1
|
||||
lines = 0
|
||||
isGameOver = false
|
||||
dropInterval = 1000L
|
||||
|
||||
// Reset scoring state
|
||||
combo = 0
|
||||
lastClearWasTetris = false
|
||||
lastClearWasPerfect = false
|
||||
lastClearWasAllClear = false
|
||||
|
||||
// Reset piece state
|
||||
holdPiece = null
|
||||
canHold = true
|
||||
bag.clear()
|
||||
|
||||
// Initialize pieces
|
||||
spawnNextPiece()
|
||||
spawnPiece()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
260
app/src/main/java/com/mintris/model/Tetromino.kt
Normal file
260
app/src/main/java/com/mintris/model/Tetromino.kt
Normal file
|
@ -0,0 +1,260 @@
|
|||
package com.mintris.model
|
||||
|
||||
/**
|
||||
* Represents a Tetris piece (Tetromino)
|
||||
*/
|
||||
enum class TetrominoType {
|
||||
I, J, L, O, S, T, Z
|
||||
}
|
||||
|
||||
class Tetromino(val type: TetrominoType) {
|
||||
|
||||
// Each tetromino has 4 rotations (0, 90, 180, 270 degrees)
|
||||
private val blocks: Array<Array<BooleanArray>> = getBlocks(type)
|
||||
private var currentRotation = 0
|
||||
|
||||
// Current position in the game grid
|
||||
var x = 0
|
||||
var y = 0
|
||||
|
||||
/**
|
||||
* Get the current shape of the tetromino based on rotation
|
||||
*/
|
||||
fun getCurrentShape(): Array<BooleanArray> {
|
||||
return blocks[currentRotation]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the width of the current tetromino shape
|
||||
*/
|
||||
fun getWidth(): Int {
|
||||
return blocks[currentRotation][0].size
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the height of the current tetromino shape
|
||||
*/
|
||||
fun getHeight(): Int {
|
||||
return blocks[currentRotation].size
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate the tetromino clockwise
|
||||
*/
|
||||
fun rotateClockwise() {
|
||||
currentRotation = (currentRotation + 1) % 4
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate the tetromino counter-clockwise
|
||||
*/
|
||||
fun rotateCounterClockwise() {
|
||||
currentRotation = (currentRotation + 3) % 4
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the tetromino's block exists at the given coordinates
|
||||
*/
|
||||
fun isBlockAt(blockX: Int, blockY: Int): Boolean {
|
||||
val shape = blocks[currentRotation]
|
||||
return if (blockY >= 0 && blockY < shape.size &&
|
||||
blockX >= 0 && blockX < shape[blockY].size) {
|
||||
shape[blockY][blockX]
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Get the block patterns for each tetromino type and all its rotations
|
||||
*/
|
||||
private fun getBlocks(type: TetrominoType): Array<Array<BooleanArray>> {
|
||||
return when (type) {
|
||||
TetrominoType.I -> arrayOf(
|
||||
// Rotation 0°
|
||||
arrayOf(
|
||||
booleanArrayOf(false, false, false, false),
|
||||
booleanArrayOf(true, true, true, true),
|
||||
booleanArrayOf(false, false, false, false),
|
||||
booleanArrayOf(false, false, false, false)
|
||||
),
|
||||
// Rotation 90°
|
||||
arrayOf(
|
||||
booleanArrayOf(false, false, true, false),
|
||||
booleanArrayOf(false, false, true, false),
|
||||
booleanArrayOf(false, false, true, false),
|
||||
booleanArrayOf(false, false, true, false)
|
||||
),
|
||||
// Rotation 180°
|
||||
arrayOf(
|
||||
booleanArrayOf(false, false, false, false),
|
||||
booleanArrayOf(false, false, false, false),
|
||||
booleanArrayOf(true, true, true, true),
|
||||
booleanArrayOf(false, false, false, false)
|
||||
),
|
||||
// Rotation 270°
|
||||
arrayOf(
|
||||
booleanArrayOf(false, true, false, false),
|
||||
booleanArrayOf(false, true, false, false),
|
||||
booleanArrayOf(false, true, false, false),
|
||||
booleanArrayOf(false, true, false, false)
|
||||
)
|
||||
)
|
||||
TetrominoType.J -> arrayOf(
|
||||
// Rotation 0°
|
||||
arrayOf(
|
||||
booleanArrayOf(true, false, false),
|
||||
booleanArrayOf(true, true, true),
|
||||
booleanArrayOf(false, false, false)
|
||||
),
|
||||
// Rotation 90°
|
||||
arrayOf(
|
||||
booleanArrayOf(false, true, true),
|
||||
booleanArrayOf(false, true, false),
|
||||
booleanArrayOf(false, true, false)
|
||||
),
|
||||
// Rotation 180°
|
||||
arrayOf(
|
||||
booleanArrayOf(false, false, false),
|
||||
booleanArrayOf(true, true, true),
|
||||
booleanArrayOf(false, false, true)
|
||||
),
|
||||
// Rotation 270°
|
||||
arrayOf(
|
||||
booleanArrayOf(false, true, false),
|
||||
booleanArrayOf(false, true, false),
|
||||
booleanArrayOf(true, true, false)
|
||||
)
|
||||
)
|
||||
TetrominoType.L -> arrayOf(
|
||||
// Rotation 0°
|
||||
arrayOf(
|
||||
booleanArrayOf(false, false, true),
|
||||
booleanArrayOf(true, true, true),
|
||||
booleanArrayOf(false, false, false)
|
||||
),
|
||||
// Rotation 90°
|
||||
arrayOf(
|
||||
booleanArrayOf(false, true, false),
|
||||
booleanArrayOf(false, true, false),
|
||||
booleanArrayOf(false, true, true)
|
||||
),
|
||||
// Rotation 180°
|
||||
arrayOf(
|
||||
booleanArrayOf(false, false, false),
|
||||
booleanArrayOf(true, true, true),
|
||||
booleanArrayOf(true, false, false)
|
||||
),
|
||||
// Rotation 270°
|
||||
arrayOf(
|
||||
booleanArrayOf(true, true, false),
|
||||
booleanArrayOf(false, true, false),
|
||||
booleanArrayOf(false, true, false)
|
||||
)
|
||||
)
|
||||
TetrominoType.O -> arrayOf(
|
||||
// All rotations are the same for O
|
||||
arrayOf(
|
||||
booleanArrayOf(false, true, true, false),
|
||||
booleanArrayOf(false, true, true, false),
|
||||
booleanArrayOf(false, false, false, false)
|
||||
),
|
||||
arrayOf(
|
||||
booleanArrayOf(false, true, true, false),
|
||||
booleanArrayOf(false, true, true, false),
|
||||
booleanArrayOf(false, false, false, false)
|
||||
),
|
||||
arrayOf(
|
||||
booleanArrayOf(false, true, true, false),
|
||||
booleanArrayOf(false, true, true, false),
|
||||
booleanArrayOf(false, false, false, false)
|
||||
),
|
||||
arrayOf(
|
||||
booleanArrayOf(false, true, true, false),
|
||||
booleanArrayOf(false, true, true, false),
|
||||
booleanArrayOf(false, false, false, false)
|
||||
)
|
||||
)
|
||||
TetrominoType.S -> arrayOf(
|
||||
// Rotation 0°
|
||||
arrayOf(
|
||||
booleanArrayOf(false, true, true),
|
||||
booleanArrayOf(true, true, false),
|
||||
booleanArrayOf(false, false, false)
|
||||
),
|
||||
// Rotation 90°
|
||||
arrayOf(
|
||||
booleanArrayOf(false, true, false),
|
||||
booleanArrayOf(false, true, true),
|
||||
booleanArrayOf(false, false, true)
|
||||
),
|
||||
// Rotation 180°
|
||||
arrayOf(
|
||||
booleanArrayOf(false, false, false),
|
||||
booleanArrayOf(false, true, true),
|
||||
booleanArrayOf(true, true, false)
|
||||
),
|
||||
// Rotation 270°
|
||||
arrayOf(
|
||||
booleanArrayOf(true, false, false),
|
||||
booleanArrayOf(true, true, false),
|
||||
booleanArrayOf(false, true, false)
|
||||
)
|
||||
)
|
||||
TetrominoType.T -> arrayOf(
|
||||
// Rotation 0°
|
||||
arrayOf(
|
||||
booleanArrayOf(false, true, false),
|
||||
booleanArrayOf(true, true, true),
|
||||
booleanArrayOf(false, false, false)
|
||||
),
|
||||
// Rotation 90°
|
||||
arrayOf(
|
||||
booleanArrayOf(false, true, false),
|
||||
booleanArrayOf(false, true, true),
|
||||
booleanArrayOf(false, true, false)
|
||||
),
|
||||
// Rotation 180°
|
||||
arrayOf(
|
||||
booleanArrayOf(false, false, false),
|
||||
booleanArrayOf(true, true, true),
|
||||
booleanArrayOf(false, true, false)
|
||||
),
|
||||
// Rotation 270°
|
||||
arrayOf(
|
||||
booleanArrayOf(false, true, false),
|
||||
booleanArrayOf(true, true, false),
|
||||
booleanArrayOf(false, true, false)
|
||||
)
|
||||
)
|
||||
TetrominoType.Z -> arrayOf(
|
||||
// Rotation 0°
|
||||
arrayOf(
|
||||
booleanArrayOf(true, true, false),
|
||||
booleanArrayOf(false, true, true),
|
||||
booleanArrayOf(false, false, false)
|
||||
),
|
||||
// Rotation 90°
|
||||
arrayOf(
|
||||
booleanArrayOf(false, false, true),
|
||||
booleanArrayOf(false, true, true),
|
||||
booleanArrayOf(false, true, false)
|
||||
),
|
||||
// Rotation 180°
|
||||
arrayOf(
|
||||
booleanArrayOf(false, false, false),
|
||||
booleanArrayOf(true, true, false),
|
||||
booleanArrayOf(false, true, true)
|
||||
),
|
||||
// Rotation 270°
|
||||
arrayOf(
|
||||
booleanArrayOf(false, true, false),
|
||||
booleanArrayOf(true, true, false),
|
||||
booleanArrayOf(true, false, false)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
10
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
</vector>
|
69
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
69
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
|
||||
<!-- T-Tetromino -->
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M36,36h12v12h-12z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M48,36h12v12h-12z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M60,36h12v12h-12z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M48,48h12v12h-12z" />
|
||||
|
||||
<!-- L-Tetromino -->
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M36,60h12v12h-12z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M36,72h12v12h-12z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M48,72h12v12h-12z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M60,72h12v12h-12z" />
|
||||
|
||||
<!-- Subtle glow effect -->
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M36,36h12v12h-12z"
|
||||
android:alpha="0.1" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M48,36h12v12h-12z"
|
||||
android:alpha="0.1" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M60,36h12v12h-12z"
|
||||
android:alpha="0.1" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M48,48h12v12h-12z"
|
||||
android:alpha="0.1" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M36,60h12v12h-12z"
|
||||
android:alpha="0.1" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M36,72h12v12h-12z"
|
||||
android:alpha="0.1" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M48,72h12v12h-12z"
|
||||
android:alpha="0.1" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M60,72h12v12h-12z"
|
||||
android:alpha="0.1" />
|
||||
</vector>
|
234
app/src/main/res/layout/activity_main.xml
Normal file
234
app/src/main/res/layout/activity_main.xml
Normal file
|
@ -0,0 +1,234 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<com.mintris.game.GameView
|
||||
android:id="@+id/gameView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<!-- HUD Container - Score, Level, Lines -->
|
||||
<LinearLayout
|
||||
android:id="@+id/hudContainer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/scoreLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/score"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/scoreText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="0"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/levelLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/level"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/levelText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="1"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/linesLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/lines"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/linesText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="0"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Next Piece Preview -->
|
||||
<LinearLayout
|
||||
android:id="@+id/nextPieceContainer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginStart="16dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nextLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/next"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.mintris.game.NextPieceView
|
||||
android:id="@+id/nextPieceView"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:background="@color/black" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Game Over overlay -->
|
||||
<LinearLayout
|
||||
android:id="@+id/gameOverContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/gameOverText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/game_over"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/finalScoreText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/playAgainButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:background="@color/transparent"
|
||||
android:text="@string/play"
|
||||
android:textColor="@color/white" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Pause overlay -->
|
||||
<LinearLayout
|
||||
android:id="@+id/pauseContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<Button
|
||||
android:id="@+id/resumeButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/transparent"
|
||||
android:text="@string/resume"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/levelSelectorContainer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/select_level"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="8dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/levelDownButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@color/transparent"
|
||||
android:text="−"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/selectedLevelText"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center"
|
||||
android:text="1"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/levelUpButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@color/transparent"
|
||||
android:text="+"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/settingsButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:background="@color/transparent"
|
||||
android:text="@string/sound_on"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp" />
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
8
app/src/main/res/values/colors.xml
Normal file
8
app/src/main/res/values/colors.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="black">#000000</color>
|
||||
<color name="white">#FFFFFF</color>
|
||||
<color name="gray_dark">#222222</color>
|
||||
<color name="gray_light">#CCCCCC</color>
|
||||
<color name="transparent">#00000000</color>
|
||||
</resources>
|
15
app/src/main/res/values/strings.xml
Normal file
15
app/src/main/res/values/strings.xml
Normal file
|
@ -0,0 +1,15 @@
|
|||
<resources>
|
||||
<string name="app_name">Mintris</string>
|
||||
<string name="game_over">GAME OVER</string>
|
||||
<string name="score">SCORE</string>
|
||||
<string name="level">LEVEL</string>
|
||||
<string name="lines">LINES</string>
|
||||
<string name="next">NEXT</string>
|
||||
<string name="play">PLAY</string>
|
||||
<string name="resume">RESUME</string>
|
||||
<string name="pause">PAUSE</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="sound_on">Sound: ON</string>
|
||||
<string name="sound_off">Sound: OFF</string>
|
||||
<string name="select_level">Select Level</string>
|
||||
</resources>
|
19
app/src/main/res/values/styles.xml
Normal file
19
app/src/main/res/values/styles.xml
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Base application theme -->
|
||||
<style name="Theme.Mintris" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<item name="colorPrimary">@color/white</item>
|
||||
<item name="colorPrimaryDark">@color/black</item>
|
||||
<item name="colorAccent">@color/white</item>
|
||||
<item name="android:windowBackground">@color/black</item>
|
||||
<item name="android:textColor">@color/white</item>
|
||||
<item name="android:fontFamily">sans-serif</item>
|
||||
</style>
|
||||
|
||||
<!-- No action bar theme -->
|
||||
<style name="Theme.Mintris.NoActionBar">
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
</style>
|
||||
</resources>
|
23
build.gradle
Normal file
23
build.gradle
Normal file
|
@ -0,0 +1,23 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
ext.kotlin_version = '2.0.0'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.9.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
26
gradle.properties
Normal file
26
gradle.properties
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app"s APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
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
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
252
gradlew
vendored
Executable file
252
gradlew
vendored
Executable file
|
@ -0,0 +1,252 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
94
gradlew.bat
vendored
Normal file
94
gradlew.bat
vendored
Normal file
|
@ -0,0 +1,94 @@
|
|||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
2
settings.gradle
Normal file
2
settings.gradle
Normal file
|
@ -0,0 +1,2 @@
|
|||
rootProject.name = "Mintris"
|
||||
include ':app'
|
Loading…
Add table
Add a link
Reference in a new issue