mirror of
https://github.com/cmclark00/mintris.git
synced 2025-05-17 23:55: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