Initial commit: Modern Tetris implementation with official rules and scoring system

This commit is contained in:
cmclark00 2025-03-26 12:44:00 -04:00
commit f4e5a9b651
27 changed files with 2898 additions and 0 deletions

59
.gitignore vendored Normal file
View 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
View file

@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "automatic"
}

124
README.md Normal file
View 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
View 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
View 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

View 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>

View 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()
}
}
}

View 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)
}
}
}

View 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)
}
}

View 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)
}
}
}
}
}
}
}

View 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
}
}

View 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)
)
)
}
}
}
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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
View 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
View 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

Binary file not shown.

View 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
View 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
View 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
View file

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