diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c5f3f6b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file diff --git a/android-app/.gradle/7.5/checksums/checksums.lock b/android-app/.gradle/7.5/checksums/checksums.lock new file mode 100644 index 0000000..4aeb5ce Binary files /dev/null and b/android-app/.gradle/7.5/checksums/checksums.lock differ diff --git a/android-app/.gradle/7.5/fileChanges/last-build.bin b/android-app/.gradle/7.5/fileChanges/last-build.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/android-app/.gradle/7.5/fileChanges/last-build.bin differ diff --git a/android-app/.gradle/7.5/fileHashes/fileHashes.lock b/android-app/.gradle/7.5/fileHashes/fileHashes.lock new file mode 100644 index 0000000..2411039 Binary files /dev/null and b/android-app/.gradle/7.5/fileHashes/fileHashes.lock differ diff --git a/android-app/.gradle/7.5/gc.properties b/android-app/.gradle/7.5/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/android-app/.gradle/8.10/checksums/checksums.lock b/android-app/.gradle/8.10/checksums/checksums.lock new file mode 100644 index 0000000..9bcb542 Binary files /dev/null and b/android-app/.gradle/8.10/checksums/checksums.lock differ diff --git a/android-app/.gradle/8.10/checksums/md5-checksums.bin b/android-app/.gradle/8.10/checksums/md5-checksums.bin new file mode 100644 index 0000000..509ec20 Binary files /dev/null and b/android-app/.gradle/8.10/checksums/md5-checksums.bin differ diff --git a/android-app/.gradle/8.10/checksums/sha1-checksums.bin b/android-app/.gradle/8.10/checksums/sha1-checksums.bin new file mode 100644 index 0000000..6bd57f3 Binary files /dev/null and b/android-app/.gradle/8.10/checksums/sha1-checksums.bin differ diff --git a/android-app/.gradle/8.10/dependencies-accessors/gc.properties b/android-app/.gradle/8.10/dependencies-accessors/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/android-app/.gradle/8.10/executionHistory/executionHistory.bin b/android-app/.gradle/8.10/executionHistory/executionHistory.bin new file mode 100644 index 0000000..1e5933b Binary files /dev/null and b/android-app/.gradle/8.10/executionHistory/executionHistory.bin differ diff --git a/android-app/.gradle/8.10/executionHistory/executionHistory.lock b/android-app/.gradle/8.10/executionHistory/executionHistory.lock new file mode 100644 index 0000000..d420e60 Binary files /dev/null and b/android-app/.gradle/8.10/executionHistory/executionHistory.lock differ diff --git a/android-app/.gradle/8.10/fileChanges/last-build.bin b/android-app/.gradle/8.10/fileChanges/last-build.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/android-app/.gradle/8.10/fileChanges/last-build.bin differ diff --git a/android-app/.gradle/8.10/fileHashes/fileHashes.bin b/android-app/.gradle/8.10/fileHashes/fileHashes.bin new file mode 100644 index 0000000..5fd80eb Binary files /dev/null and b/android-app/.gradle/8.10/fileHashes/fileHashes.bin differ diff --git a/android-app/.gradle/8.10/fileHashes/fileHashes.lock b/android-app/.gradle/8.10/fileHashes/fileHashes.lock new file mode 100644 index 0000000..eaa5424 Binary files /dev/null and b/android-app/.gradle/8.10/fileHashes/fileHashes.lock differ diff --git a/android-app/.gradle/8.10/gc.properties b/android-app/.gradle/8.10/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/android-app/.gradle/8.11.1/checksums/checksums.lock b/android-app/.gradle/8.11.1/checksums/checksums.lock new file mode 100644 index 0000000..3a2f1da Binary files /dev/null and b/android-app/.gradle/8.11.1/checksums/checksums.lock differ diff --git a/android-app/.gradle/8.11.1/checksums/md5-checksums.bin b/android-app/.gradle/8.11.1/checksums/md5-checksums.bin new file mode 100644 index 0000000..9f7f27d Binary files /dev/null and b/android-app/.gradle/8.11.1/checksums/md5-checksums.bin differ diff --git a/android-app/.gradle/8.11.1/checksums/sha1-checksums.bin b/android-app/.gradle/8.11.1/checksums/sha1-checksums.bin new file mode 100644 index 0000000..27b3854 Binary files /dev/null and b/android-app/.gradle/8.11.1/checksums/sha1-checksums.bin differ diff --git a/android-app/.gradle/8.11.1/executionHistory/executionHistory.bin b/android-app/.gradle/8.11.1/executionHistory/executionHistory.bin new file mode 100644 index 0000000..1e65897 Binary files /dev/null and b/android-app/.gradle/8.11.1/executionHistory/executionHistory.bin differ diff --git a/android-app/.gradle/8.11.1/executionHistory/executionHistory.lock b/android-app/.gradle/8.11.1/executionHistory/executionHistory.lock new file mode 100644 index 0000000..152df37 Binary files /dev/null and b/android-app/.gradle/8.11.1/executionHistory/executionHistory.lock differ diff --git a/android-app/.gradle/8.11.1/fileChanges/last-build.bin b/android-app/.gradle/8.11.1/fileChanges/last-build.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/android-app/.gradle/8.11.1/fileChanges/last-build.bin differ diff --git a/android-app/.gradle/8.11.1/fileHashes/fileHashes.bin b/android-app/.gradle/8.11.1/fileHashes/fileHashes.bin new file mode 100644 index 0000000..9dde153 Binary files /dev/null and b/android-app/.gradle/8.11.1/fileHashes/fileHashes.bin differ diff --git a/android-app/.gradle/8.11.1/fileHashes/fileHashes.lock b/android-app/.gradle/8.11.1/fileHashes/fileHashes.lock new file mode 100644 index 0000000..8190865 Binary files /dev/null and b/android-app/.gradle/8.11.1/fileHashes/fileHashes.lock differ diff --git a/android-app/.gradle/8.11.1/fileHashes/resourceHashesCache.bin b/android-app/.gradle/8.11.1/fileHashes/resourceHashesCache.bin new file mode 100644 index 0000000..92a1969 Binary files /dev/null and b/android-app/.gradle/8.11.1/fileHashes/resourceHashesCache.bin differ diff --git a/android-app/.gradle/8.11.1/gc.properties b/android-app/.gradle/8.11.1/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/android-app/.gradle/8.8/checksums/checksums.lock b/android-app/.gradle/8.8/checksums/checksums.lock new file mode 100644 index 0000000..7f0b8e9 Binary files /dev/null and b/android-app/.gradle/8.8/checksums/checksums.lock differ diff --git a/android-app/.gradle/8.8/checksums/md5-checksums.bin b/android-app/.gradle/8.8/checksums/md5-checksums.bin new file mode 100644 index 0000000..efd3160 Binary files /dev/null and b/android-app/.gradle/8.8/checksums/md5-checksums.bin differ diff --git a/android-app/.gradle/8.8/checksums/sha1-checksums.bin b/android-app/.gradle/8.8/checksums/sha1-checksums.bin new file mode 100644 index 0000000..0c5c37d Binary files /dev/null and b/android-app/.gradle/8.8/checksums/sha1-checksums.bin differ diff --git a/android-app/.gradle/8.8/dependencies-accessors/gc.properties b/android-app/.gradle/8.8/dependencies-accessors/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/android-app/.gradle/8.8/fileChanges/last-build.bin b/android-app/.gradle/8.8/fileChanges/last-build.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/android-app/.gradle/8.8/fileChanges/last-build.bin differ diff --git a/android-app/.gradle/8.8/fileHashes/fileHashes.bin b/android-app/.gradle/8.8/fileHashes/fileHashes.bin new file mode 100644 index 0000000..689e596 Binary files /dev/null and b/android-app/.gradle/8.8/fileHashes/fileHashes.bin differ diff --git a/android-app/.gradle/8.8/fileHashes/fileHashes.lock b/android-app/.gradle/8.8/fileHashes/fileHashes.lock new file mode 100644 index 0000000..28f50b2 Binary files /dev/null and b/android-app/.gradle/8.8/fileHashes/fileHashes.lock differ diff --git a/android-app/.gradle/8.8/gc.properties b/android-app/.gradle/8.8/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/android-app/.gradle/8.9/checksums/checksums.lock b/android-app/.gradle/8.9/checksums/checksums.lock new file mode 100644 index 0000000..347b45b Binary files /dev/null and b/android-app/.gradle/8.9/checksums/checksums.lock differ diff --git a/android-app/.gradle/8.9/dependencies-accessors/gc.properties b/android-app/.gradle/8.9/dependencies-accessors/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/android-app/.gradle/8.9/executionHistory/executionHistory.lock b/android-app/.gradle/8.9/executionHistory/executionHistory.lock new file mode 100644 index 0000000..1716945 Binary files /dev/null and b/android-app/.gradle/8.9/executionHistory/executionHistory.lock differ diff --git a/android-app/.gradle/8.9/fileChanges/last-build.bin b/android-app/.gradle/8.9/fileChanges/last-build.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/android-app/.gradle/8.9/fileChanges/last-build.bin differ diff --git a/android-app/.gradle/8.9/fileHashes/fileHashes.lock b/android-app/.gradle/8.9/fileHashes/fileHashes.lock new file mode 100644 index 0000000..bbe8235 Binary files /dev/null and b/android-app/.gradle/8.9/fileHashes/fileHashes.lock differ diff --git a/android-app/.gradle/8.9/gc.properties b/android-app/.gradle/8.9/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/android-app/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/android-app/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 0000000..8909f05 Binary files /dev/null and b/android-app/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/android-app/.gradle/buildOutputCleanup/cache.properties b/android-app/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 0000000..86e9063 --- /dev/null +++ b/android-app/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Tue Mar 25 23:50:46 EDT 2025 +gradle.version=8.11.1 diff --git a/android-app/.gradle/buildOutputCleanup/outputFiles.bin b/android-app/.gradle/buildOutputCleanup/outputFiles.bin new file mode 100644 index 0000000..86fc029 Binary files /dev/null and b/android-app/.gradle/buildOutputCleanup/outputFiles.bin differ diff --git a/android-app/.gradle/config.properties b/android-app/.gradle/config.properties new file mode 100644 index 0000000..f2e3794 --- /dev/null +++ b/android-app/.gradle/config.properties @@ -0,0 +1,2 @@ +#Tue Mar 25 23:36:25 EDT 2025 +java.home=/home/corey/Downloads/android-studio-2024.3.1.13-linux/android-studio/jbr diff --git a/android-app/.gradle/file-system.probe b/android-app/.gradle/file-system.probe new file mode 100644 index 0000000..c7a2e14 Binary files /dev/null and b/android-app/.gradle/file-system.probe differ diff --git a/android-app/.gradle/nb-cache/android-app-623140067/project-info.ser b/android-app/.gradle/nb-cache/android-app-623140067/project-info.ser new file mode 100644 index 0000000..5c2e123 Binary files /dev/null and b/android-app/.gradle/nb-cache/android-app-623140067/project-info.ser differ diff --git a/android-app/.gradle/nb-cache/app-271321845/project-info.ser b/android-app/.gradle/nb-cache/app-271321845/project-info.ser new file mode 100644 index 0000000..5de0ac0 Binary files /dev/null and b/android-app/.gradle/nb-cache/app-271321845/project-info.ser differ diff --git a/android-app/.gradle/nb-cache/subprojects.ser b/android-app/.gradle/nb-cache/subprojects.ser new file mode 100644 index 0000000..faa7cf7 Binary files /dev/null and b/android-app/.gradle/nb-cache/subprojects.ser differ diff --git a/android-app/.gradle/nb-cache/trust/388F7AB02DFD0183E3FD2FB1B5B4206B0C7407CAFFD7F76F3C1EF65077068EC5 b/android-app/.gradle/nb-cache/trust/388F7AB02DFD0183E3FD2FB1B5B4206B0C7407CAFFD7F76F3C1EF65077068EC5 new file mode 100644 index 0000000..f62917f --- /dev/null +++ b/android-app/.gradle/nb-cache/trust/388F7AB02DFD0183E3FD2FB1B5B4206B0C7407CAFFD7F76F3C1EF65077068EC5 @@ -0,0 +1 @@ +8B64E587393F94313EFF41233F48D4848E985369BD2E4B833B25B3765823A6DB diff --git a/android-app/.gradle/vcs-1/gc.properties b/android-app/.gradle/vcs-1/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/android-app/.idea/.gitignore b/android-app/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/android-app/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/android-app/.idea/.name b/android-app/.idea/.name new file mode 100644 index 0000000..8f54013 --- /dev/null +++ b/android-app/.idea/.name @@ -0,0 +1 @@ +Tetris3D \ No newline at end of file diff --git a/android-app/.idea/AndroidProjectSystem.xml b/android-app/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/android-app/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android-app/.idea/appInsightsSettings.xml b/android-app/.idea/appInsightsSettings.xml new file mode 100644 index 0000000..371f2e2 --- /dev/null +++ b/android-app/.idea/appInsightsSettings.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android-app/.idea/caches/deviceStreaming.xml b/android-app/.idea/caches/deviceStreaming.xml new file mode 100644 index 0000000..9e9ba09 --- /dev/null +++ b/android-app/.idea/caches/deviceStreaming.xml @@ -0,0 +1,607 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android-app/.idea/compiler.xml b/android-app/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/android-app/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android-app/.idea/deploymentTargetSelector.xml b/android-app/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/android-app/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/android-app/.idea/gradle.xml b/android-app/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/android-app/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android-app/.idea/kotlinc.xml b/android-app/.idea/kotlinc.xml new file mode 100644 index 0000000..2b8a50f --- /dev/null +++ b/android-app/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android-app/.idea/migrations.xml b/android-app/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/android-app/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/android-app/.idea/misc.xml b/android-app/.idea/misc.xml new file mode 100644 index 0000000..74dd639 --- /dev/null +++ b/android-app/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/android-app/.idea/runConfigurations.xml b/android-app/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/android-app/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android-app/.idea/vcs.xml b/android-app/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/android-app/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android-app/README.md b/android-app/README.md new file mode 100644 index 0000000..b12cb8b --- /dev/null +++ b/android-app/README.md @@ -0,0 +1,57 @@ +# 3D Tetris - Android App + +This is the native Android implementation of the 3D Tetris game. The mobile-specific optimizations from the web app have been removed, and instead, this native app has been created to provide a better experience on Android devices. + +## Project Structure + +- `app/src/main/java/com/tetris3d/` - Contains all Java/Kotlin source files + - `MainActivity.kt` - Main activity that hosts the game + - `game/` - Game logic implementation + - `views/` - Custom views for rendering game elements + +- `app/src/main/res/` - Contains all resources + - `layout/` - XML layouts for the UI + - `values/` - String resources, colors, and themes + - `drawable/` - Icon and image resources + +## Features + +- 3D Tetris gameplay with modern graphics +- Customizable options for 3D effects and animations +- Physical button controls optimized for touch +- Score tracking and level progression +- Game state persistence + +## Required Implementation + +The following components still need to be implemented to complete the Android app: + +1. TetrisGame class - Core game logic ported from JavaScript +2. TetrisGameView - Custom view for rendering the game +3. NextPieceView - Custom view for rendering the next piece preview +4. Tetromino classes - Classes for different tetromino pieces +5. Game renderer - OpenGL ES or Canvas-based renderer for the 3D effects + +## Dependencies + +- AndroidX libraries for UI components +- Kotlin coroutines for game loop threading + +## Building and Running + +1. Open the project in Android Studio +2. Build the project using Gradle +3. Deploy to an Android device or emulator + +## Development Process + +The Android app was created by: + +1. Analyzing the web implementation of the game +2. Removing mobile-specific optimizations from the web code +3. Creating a native Android app structure +4. Implementing the UI layouts and resources +5. Porting the core game logic from JavaScript to Kotlin +6. Adding Android-specific features and optimizations + +The core game mechanics are kept identical to the web version, ensuring a consistent experience across platforms. \ No newline at end of file diff --git a/android-app/app/build.gradle b/android-app/app/build.gradle new file mode 100644 index 0000000..dd59f99 --- /dev/null +++ b/android-app/app/build.gradle @@ -0,0 +1,50 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'com.tetris3d' + compileSdk 33 + + defaultConfig { + applicationId "com.tetris3d" + minSdk 26 + targetSdk 33 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + buildFeatures { + viewBinding true + } +} + +dependencies { + implementation 'androidx.core:core-ktx:1.10.1' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.9.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1' + implementation 'androidx.navigation:navigation-fragment-ktx:2.6.0' + implementation 'androidx.navigation:navigation-ui-ktx:2.6.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' +} \ No newline at end of file diff --git a/android-app/app/src/main/AndroidManifest.xml b/android-app/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3f8982d --- /dev/null +++ b/android-app/app/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + diff --git a/android-app/app/src/main/java/com/tetris3d/MainActivity.kt b/android-app/app/src/main/java/com/tetris3d/MainActivity.kt new file mode 100644 index 0000000..634ac69 --- /dev/null +++ b/android-app/app/src/main/java/com/tetris3d/MainActivity.kt @@ -0,0 +1,254 @@ +package com.tetris3d + +import android.app.Dialog +import android.os.Bundle +import android.view.View +import android.widget.Button +import android.widget.NumberPicker +import android.widget.SeekBar +import android.widget.Switch +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import com.tetris3d.game.GameOptions +import com.tetris3d.game.TetrisGame +import com.tetris3d.views.NextPieceView +import com.tetris3d.views.TetrisGameView + +class MainActivity : AppCompatActivity() { + private lateinit var gameView: TetrisGameView + private lateinit var nextPieceView: NextPieceView + private lateinit var scoreText: TextView + private lateinit var linesText: TextView + private lateinit var levelText: TextView + private lateinit var startButton: Button + private lateinit var pauseButton: Button + private lateinit var optionsButton: Button + + private lateinit var tetrisGame: TetrisGame + private lateinit var gameOptions: GameOptions + + private var gameOverDialog: Dialog? = null + private var optionsDialog: Dialog? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + initViews() + initGameOptions() + initGame() + setupButtonListeners() + } + + private fun initViews() { + gameView = findViewById(R.id.tetrisGameView) + nextPieceView = findViewById(R.id.nextPieceView) + scoreText = findViewById(R.id.scoreText) + linesText = findViewById(R.id.linesText) + levelText = findViewById(R.id.levelText) + startButton = findViewById(R.id.startButton) + pauseButton = findViewById(R.id.pauseButton) + optionsButton = findViewById(R.id.optionsButton) + } + + private fun initGameOptions() { + gameOptions = GameOptions( + enable3DEffects = true, + enableSpinAnimations = true, + animationSpeed = 0.05f, + startingLevel = 1 + ) + + // Load saved options from SharedPreferences + val prefs = getSharedPreferences("TetrisOptions", MODE_PRIVATE) + gameOptions.enable3DEffects = prefs.getBoolean("enable3DEffects", true) + gameOptions.enableSpinAnimations = prefs.getBoolean("enableSpinAnimations", true) + gameOptions.animationSpeed = prefs.getFloat("animationSpeed", 0.05f) + gameOptions.startingLevel = prefs.getInt("startingLevel", 1) + } + + private fun initGame() { + tetrisGame = TetrisGame(gameOptions) + gameView.setGame(tetrisGame) + nextPieceView.setGame(tetrisGame) + + tetrisGame.setGameStateListener(object : TetrisGame.GameStateListener { + override fun onScoreChanged(score: Int) { + runOnUiThread { + scoreText.text = score.toString() + } + } + + override fun onLinesChanged(lines: Int) { + runOnUiThread { + linesText.text = lines.toString() + } + } + + override fun onLevelChanged(level: Int) { + runOnUiThread { + levelText.text = level.toString() + } + } + + override fun onGameOver(finalScore: Int) { + runOnUiThread { + showGameOverDialog(finalScore) + } + } + + override fun onNextPieceChanged() { + runOnUiThread { + nextPieceView.invalidate() + } + } + }) + + // Start a new game automatically + tetrisGame.startNewGame() + updateControls() + } + + private fun setupButtonListeners() { + startButton.setOnClickListener { + if (tetrisGame.isGameOver) { + tetrisGame.startNewGame() + } else { + tetrisGame.start() + } + updateControls() + } + + pauseButton.setOnClickListener { + if (tetrisGame.isRunning) { + tetrisGame.pause() + } else { + tetrisGame.resume() + } + updateControls() + } + + optionsButton.setOnClickListener { + showOptionsDialog() + } + } + + private fun updateControls() { + if (tetrisGame.isRunning) { + startButton.visibility = View.GONE + pauseButton.text = getString(R.string.pause) + } else if (tetrisGame.isGameOver) { + startButton.visibility = View.VISIBLE + startButton.text = getString(R.string.start) + pauseButton.text = getString(R.string.pause) + } else { + startButton.visibility = View.GONE + pauseButton.text = getString(R.string.start) + } + } + + private fun showGameOverDialog(finalScore: Int) { + if (gameOverDialog != null && gameOverDialog!!.isShowing) { + gameOverDialog!!.dismiss() + } + + val view = layoutInflater.inflate(R.layout.dialog_game_over, null) + val scoreText = view.findViewById(R.id.textFinalScore) + val playAgainButton = view.findViewById(R.id.btnPlayAgain) + + scoreText.text = finalScore.toString() + + gameOverDialog = AlertDialog.Builder(this) + .setView(view) + .setCancelable(false) + .create() + + playAgainButton.setOnClickListener { + gameOverDialog?.dismiss() + tetrisGame.startNewGame() + updateControls() + } + + gameOverDialog?.show() + } + + private fun showOptionsDialog() { + if (optionsDialog != null && optionsDialog!!.isShowing) { + optionsDialog!!.dismiss() + } + + val view = layoutInflater.inflate(R.layout.dialog_options, null) + + val switch3dEffects = view.findViewById(R.id.switch3dEffects) + val switchSpinAnimations = view.findViewById(R.id.switchSpinAnimations) + val seekBarSpeed = view.findViewById(R.id.seekBarAnimationSpeed) + val numberPickerLevel = view.findViewById(R.id.numberPickerLevel) + val btnApply = view.findViewById(R.id.btnApplyOptions) + val btnClose = view.findViewById(R.id.btnCloseOptions) + + // Set up controls with current options + switch3dEffects.isChecked = gameOptions.enable3DEffects + switchSpinAnimations.isChecked = gameOptions.enableSpinAnimations + + // Convert animation speed (0.01-0.1) to progress (0-100) + val progress = ((gameOptions.animationSpeed - 0.01f) / 0.09f * 100).toInt() + seekBarSpeed.progress = progress + + // Set up level picker + numberPickerLevel.minValue = 1 + numberPickerLevel.maxValue = 10 + numberPickerLevel.value = gameOptions.startingLevel + + optionsDialog = AlertDialog.Builder(this) + .setView(view) + .setCancelable(true) + .create() + + btnApply.setOnClickListener { + // Save new options + gameOptions.enable3DEffects = switch3dEffects.isChecked + gameOptions.enableSpinAnimations = switchSpinAnimations.isChecked + + // Convert progress (0-100) to animation speed (0.01-0.1) + val animationSpeed = 0.01f + (seekBarSpeed.progress / 100f * 0.09f) + gameOptions.animationSpeed = animationSpeed + + gameOptions.startingLevel = numberPickerLevel.value + + // Apply options to game + tetrisGame.updateOptions(gameOptions) + + // Save options to SharedPreferences + val prefs = getSharedPreferences("TetrisOptions", MODE_PRIVATE) + prefs.edit().apply { + putBoolean("enable3DEffects", gameOptions.enable3DEffects) + putBoolean("enableSpinAnimations", gameOptions.enableSpinAnimations) + putFloat("animationSpeed", gameOptions.animationSpeed) + putInt("startingLevel", gameOptions.startingLevel) + apply() + } + + optionsDialog?.dismiss() + } + + btnClose.setOnClickListener { + optionsDialog?.dismiss() + } + + optionsDialog?.show() + } + + override fun onPause() { + super.onPause() + if (tetrisGame.isRunning) { + tetrisGame.pause() + updateControls() + } + } + + override fun onDestroy() { + super.onDestroy() + tetrisGame.stop() + } +} \ No newline at end of file diff --git a/android-app/app/src/main/java/com/tetris3d/game/GameOptions.kt b/android-app/app/src/main/java/com/tetris3d/game/GameOptions.kt new file mode 100644 index 0000000..30ba2c7 --- /dev/null +++ b/android-app/app/src/main/java/com/tetris3d/game/GameOptions.kt @@ -0,0 +1,11 @@ +package com.tetris3d.game + +/** + * Class that holds game configuration options + */ +data class GameOptions( + var enable3DEffects: Boolean = true, + var enableSpinAnimations: Boolean = true, + var animationSpeed: Float = 0.05f, + var startingLevel: Int = 1 +) \ No newline at end of file diff --git a/android-app/app/src/main/java/com/tetris3d/game/TetrisGame.kt b/android-app/app/src/main/java/com/tetris3d/game/TetrisGame.kt new file mode 100644 index 0000000..fe6f0fe --- /dev/null +++ b/android-app/app/src/main/java/com/tetris3d/game/TetrisGame.kt @@ -0,0 +1,722 @@ +package com.tetris3d.game + +import android.os.Handler +import android.os.Looper +import kotlin.random.Random + +/** + * Main class that handles Tetris game logic + */ +class TetrisGame(private var options: GameOptions) { + + companion object { + const val ROWS = 20 + const val COLS = 10 + const val EMPTY = "black" + + // 3D rotation directions + private const val ROTATION_X = 0 + private const val ROTATION_Y = 1 + } + + // Game state + var isRunning = false + var isGameOver = false + private var score = 0 + private var lines = 0 + private var level = options.startingLevel + + // Board representation + private val board = Array(ROWS) { Array(COLS) { EMPTY } } + + // Piece definitions + private val pieces = listOf( + // I piece - line + listOf( + arrayOf( + arrayOf(0, 0, 0, 0), + arrayOf(1, 1, 1, 1), + arrayOf(0, 0, 0, 0), + arrayOf(0, 0, 0, 0) + ), + arrayOf( + arrayOf(0, 0, 1, 0), + arrayOf(0, 0, 1, 0), + arrayOf(0, 0, 1, 0), + arrayOf(0, 0, 1, 0) + ), + arrayOf( + arrayOf(0, 0, 0, 0), + arrayOf(0, 0, 0, 0), + arrayOf(1, 1, 1, 1), + arrayOf(0, 0, 0, 0) + ), + arrayOf( + arrayOf(0, 1, 0, 0), + arrayOf(0, 1, 0, 0), + arrayOf(0, 1, 0, 0), + arrayOf(0, 1, 0, 0) + ) + ), + // J piece + listOf( + arrayOf( + arrayOf(1, 0, 0), + arrayOf(1, 1, 1), + arrayOf(0, 0, 0) + ), + arrayOf( + arrayOf(0, 1, 1), + arrayOf(0, 1, 0), + arrayOf(0, 1, 0) + ), + arrayOf( + arrayOf(0, 0, 0), + arrayOf(1, 1, 1), + arrayOf(0, 0, 1) + ), + arrayOf( + arrayOf(0, 1, 0), + arrayOf(0, 1, 0), + arrayOf(1, 1, 0) + ) + ), + // L piece + listOf( + arrayOf( + arrayOf(0, 0, 1), + arrayOf(1, 1, 1), + arrayOf(0, 0, 0) + ), + arrayOf( + arrayOf(0, 1, 0), + arrayOf(0, 1, 0), + arrayOf(0, 1, 1) + ), + arrayOf( + arrayOf(0, 0, 0), + arrayOf(1, 1, 1), + arrayOf(1, 0, 0) + ), + arrayOf( + arrayOf(1, 1, 0), + arrayOf(0, 1, 0), + arrayOf(0, 1, 0) + ) + ), + // O piece - square + listOf( + arrayOf( + arrayOf(0, 0, 0, 0), + arrayOf(0, 1, 1, 0), + arrayOf(0, 1, 1, 0), + arrayOf(0, 0, 0, 0) + ) + ), + // S piece + listOf( + arrayOf( + arrayOf(0, 1, 1), + arrayOf(1, 1, 0), + arrayOf(0, 0, 0) + ), + arrayOf( + arrayOf(0, 1, 0), + arrayOf(0, 1, 1), + arrayOf(0, 0, 1) + ), + arrayOf( + arrayOf(0, 0, 0), + arrayOf(0, 1, 1), + arrayOf(1, 1, 0) + ), + arrayOf( + arrayOf(1, 0, 0), + arrayOf(1, 1, 0), + arrayOf(0, 1, 0) + ) + ), + // T piece + listOf( + arrayOf( + arrayOf(0, 1, 0), + arrayOf(1, 1, 1), + arrayOf(0, 0, 0) + ), + arrayOf( + arrayOf(0, 1, 0), + arrayOf(0, 1, 1), + arrayOf(0, 1, 0) + ), + arrayOf( + arrayOf(0, 0, 0), + arrayOf(1, 1, 1), + arrayOf(0, 1, 0) + ), + arrayOf( + arrayOf(0, 1, 0), + arrayOf(1, 1, 0), + arrayOf(0, 1, 0) + ) + ), + // Z piece + listOf( + arrayOf( + arrayOf(1, 1, 0), + arrayOf(0, 1, 1), + arrayOf(0, 0, 0) + ), + arrayOf( + arrayOf(0, 0, 1), + arrayOf(0, 1, 1), + arrayOf(0, 1, 0) + ), + arrayOf( + arrayOf(0, 0, 0), + arrayOf(1, 1, 0), + arrayOf(0, 1, 1) + ), + arrayOf( + arrayOf(0, 1, 0), + arrayOf(1, 1, 0), + arrayOf(1, 0, 0) + ) + ) + ) + + // 3D rotation state + private var rotation3DX = 0 + private var rotation3DY = 0 + private val maxRotation3D = 4 // Increased from typical 2 to allow for more granular rotation + + // 3D rotation animation + private var isRotating = false + private var rotationProgress = 0f + private var targetRotationX = 0 + private var targetRotationY = 0 + private var currentRotation3DX = 0f + private var currentRotation3DY = 0f + + // Piece colors + private val colors = listOf( + "#00FFFF", // cyan - I + "#0000FF", // blue - J + "#FFA500", // orange - L + "#FFFF00", // yellow - O + "#00FF00", // green - S + "#800080", // purple - T + "#FF0000" // red - Z + ) + + // Current piece state + private var currentPiece: Int = 0 + private var currentRotation: Int = 0 + private var currentX: Int = 0 + private var currentY: Int = 0 + private var currentColor: String = "" + + // Next piece + private var nextPiece: Int = 0 + private var nextColor: String = "" + + // Random bag implementation + private val pieceBag = mutableListOf() + private val nextBag = mutableListOf() + + // Game loop + private val gameHandler = Handler(Looper.getMainLooper()) + private val gameRunnable = object : Runnable { + override fun run() { + if (isRunning && !isGameOver) { + // Update rotation animation + updateRotation() + + // Move the current piece down + if (!moveDown()) { + // If can't move down, lock the piece + lockPiece() + clearRows() + if (!createNewPiece()) { + gameOver() + } + } + gameHandler.postDelayed(this, getDropInterval()) + } + } + } + + private var gameStateListener: GameStateListener? = null + + interface GameStateListener { + fun onScoreChanged(score: Int) + fun onLinesChanged(lines: Int) + fun onLevelChanged(level: Int) + fun onGameOver(finalScore: Int) + fun onNextPieceChanged() + } + + fun setGameStateListener(listener: GameStateListener) { + this.gameStateListener = listener + } + + fun start() { + if (!isRunning) { + isRunning = true + isGameOver = false + gameHandler.postDelayed(gameRunnable, getDropInterval()) + } + } + + fun pause() { + isRunning = false + gameHandler.removeCallbacks(gameRunnable) + } + + fun resume() { + if (!isGameOver) { + isRunning = true + gameHandler.postDelayed(gameRunnable, getDropInterval()) + } + } + + fun stop() { + isRunning = false + gameHandler.removeCallbacks(gameRunnable) + } + + fun startNewGame() { + // Reset game state + isRunning = true + isGameOver = false + score = 0 + lines = 0 + level = options.startingLevel + + // Reset 3D rotation state + rotation3DX = 0 + rotation3DY = 0 + + // Clear the board + for (r in 0 until ROWS) { + for (c in 0 until COLS) { + board[r][c] = EMPTY + } + } + + // Reset piece bags + pieceBag.clear() + nextBag.clear() + + // Create first piece + generateBag() + createNewPiece() + + // Update UI + gameStateListener?.onScoreChanged(score) + gameStateListener?.onLinesChanged(lines) + gameStateListener?.onLevelChanged(level) + + // Start game loop + gameHandler.removeCallbacks(gameRunnable) + gameHandler.postDelayed(gameRunnable, getDropInterval()) + } + + fun updateOptions(options: GameOptions) { + this.options = options + } + + // Game control methods + fun moveLeft(): Boolean { + if (isRunning && !isGameOver) { + if (!checkCollision(currentX - 1, currentY, getCurrentPieceArray())) { + currentX-- + return true + } + } + return false + } + + fun moveRight(): Boolean { + if (isRunning && !isGameOver) { + if (!checkCollision(currentX + 1, currentY, getCurrentPieceArray())) { + currentX++ + return true + } + } + return false + } + + fun moveDown(): Boolean { + if (isRunning && !isGameOver) { + if (!checkCollision(currentX, currentY + 1, getCurrentPieceArray())) { + currentY++ + return true + } + } + return false + } + + fun rotate(): Boolean { + if (isRunning && !isGameOver) { + val nextRotation = (currentRotation + 1) % pieces[currentPiece].size + val nextPattern = pieces[currentPiece][nextRotation] + + if (!checkCollision(currentX, currentY, nextPattern)) { + currentRotation = nextRotation + return true + } else { + // Try wall kicks + // Try moving right + if (!checkCollision(currentX + 1, currentY, nextPattern)) { + currentX++ + currentRotation = nextRotation + return true + } + // Try moving left + if (!checkCollision(currentX - 1, currentY, nextPattern)) { + currentX-- + currentRotation = nextRotation + return true + } + // Try moving up (for I piece mostly) + if (!checkCollision(currentX, currentY - 1, nextPattern)) { + currentY-- + currentRotation = nextRotation + return true + } + } + } + return false + } + + fun hardDrop(): Boolean { + if (isRunning && !isGameOver) { + while (moveDown()) {} + lockPiece() + clearRows() + if (!createNewPiece()) { + gameOver() + } else { + // Add extra points for hard drop + score += 2 + gameStateListener?.onScoreChanged(score) + } + return true + } + return false + } + + fun rotate3DX(): Boolean { + if (isRunning && !isGameOver && options.enable3DEffects) { + // In 3D, rotating along X would change the way the piece appears from front/back + rotation3DX = (rotation3DX + 1) % maxRotation3D + + // Start rotation animation + if (options.enableSpinAnimations) { + isRotating = true + targetRotationX = rotation3DX + rotationProgress = 0f + } + + // If it's a quarter or three-quarter rotation, actually change the piece orientation + if (rotation3DX % (maxRotation3D / 2) == 1) { + // This simulates a 3D rotation by performing a 2D rotation + return rotate() + } + + // Add extra score for 3D rotations when they don't result in a piece rotation + score += 1 + gameStateListener?.onScoreChanged(score) + return true + } + return false + } + + fun rotate3DY(): Boolean { + if (isRunning && !isGameOver && options.enable3DEffects) { + // In 3D, rotating along Y would change the way the piece appears from left/right + rotation3DY = (rotation3DY + 1) % maxRotation3D + + // Start rotation animation + if (options.enableSpinAnimations) { + isRotating = true + targetRotationY = rotation3DY + rotationProgress = 0f + } + + // If it's a quarter or three-quarter rotation, actually change the piece orientation + if (rotation3DY % (maxRotation3D / 2) == 1) { + // This simulates a 3D rotation by performing a 2D rotation in the opposite direction + val nextRotation = (currentRotation - 1 + pieces[currentPiece].size) % pieces[currentPiece].size + val nextPattern = pieces[currentPiece][nextRotation] + + if (!checkCollision(currentX, currentY, nextPattern)) { + currentRotation = nextRotation + return true + } else { + // Try wall kicks + // Try moving right + if (!checkCollision(currentX + 1, currentY, nextPattern)) { + currentX++ + currentRotation = nextRotation + return true + } + // Try moving left + if (!checkCollision(currentX - 1, currentY, nextPattern)) { + currentX-- + currentRotation = nextRotation + return true + } + // Try moving up (for I piece mostly) + if (!checkCollision(currentX, currentY - 1, nextPattern)) { + currentY-- + currentRotation = nextRotation + return true + } + } + } + + // Add extra score for 3D rotations when they don't result in a piece rotation + score += 1 + gameStateListener?.onScoreChanged(score) + return true + } + return false + } + + // Method to update rotation animation + fun updateRotation() { + if (isRotating && options.enableSpinAnimations) { + rotationProgress += options.animationSpeed + if (rotationProgress >= 1f) { + // Animation complete + rotationProgress = 1f + isRotating = false + currentRotation3DX = targetRotationX.toFloat() + currentRotation3DY = targetRotationY.toFloat() + } else { + // Smooth interpolation for rotation + currentRotation3DX = rotation3DX * rotationProgress + + (rotation3DX - 1 + maxRotation3D) % maxRotation3D * (1f - rotationProgress) + currentRotation3DY = rotation3DY * rotationProgress + + (rotation3DY - 1 + maxRotation3D) % maxRotation3D * (1f - rotationProgress) + } + } + } + + private fun generateBag() { + if (pieceBag.isEmpty()) { + // If both bags are empty, initialize both + if (nextBag.isEmpty()) { + // Fill the next bag with 0-6 (all 7 pieces) in random order + val tempBag = (0..6).toMutableList() + tempBag.shuffle() + nextBag.addAll(tempBag) + } + // Move the next bag to current and create a new next bag + pieceBag.addAll(nextBag) + nextBag.clear() + + // Fill the next bag again + val tempBag = (0..6).toMutableList() + tempBag.shuffle() + nextBag.addAll(tempBag) + } + } + + private fun getNextPieceFromBag(): Int { + if (pieceBag.isEmpty()) { + generateBag() + } + return pieceBag.removeAt(0) + } + + private fun createNewPiece(): Boolean { + // Get next piece from bag + currentPiece = nextPiece + currentColor = nextColor + + // Generate next piece + nextPiece = getNextPieceFromBag() + nextColor = colors[nextPiece] + + // If it's the first piece, generate the current one too + if (currentColor.isEmpty()) { + currentPiece = getNextPieceFromBag() + currentColor = colors[currentPiece] + } + + // Reset position and rotation + currentRotation = 0 + currentX = COLS / 2 - 2 + currentY = 0 + + // Notify next piece changed + gameStateListener?.onNextPieceChanged() + + // Check if game over (collision at starting position) + return !checkCollision(currentX, currentY, getCurrentPieceArray()) + } + + private fun lockPiece() { + val piece = getCurrentPieceArray() + + for (r in piece.indices) { + for (c in piece[r].indices) { + if (piece[r][c] == 1) { + val boardRow = currentY + r + val boardCol = currentX + c + + if (boardRow >= 0 && boardRow < ROWS && boardCol >= 0 && boardCol < COLS) { + board[boardRow][boardCol] = currentColor + } + } + } + } + } + + private fun clearRows() { + var linesCleared = 0 + + for (r in 0 until ROWS) { + var rowFull = true + + for (c in 0 until COLS) { + if (board[r][c] == EMPTY) { + rowFull = false + break + } + } + + if (rowFull) { + // Move all rows above down + for (y in r downTo 1) { + for (c in 0 until COLS) { + board[y][c] = board[y - 1][c] + } + } + + // Clear top row + for (c in 0 until COLS) { + board[0][c] = EMPTY + } + + linesCleared++ + } + } + + if (linesCleared > 0) { + // Update lines and score + lines += linesCleared + + // Calculate score based on lines cleared and level + when (linesCleared) { + 1 -> score += 100 * level + 2 -> score += 300 * level + 3 -> score += 500 * level + 4 -> score += 800 * level + } + + // Update level (every 10 lines) + level = (lines / 10) + options.startingLevel + + // Notify listeners + gameStateListener?.onScoreChanged(score) + gameStateListener?.onLinesChanged(lines) + gameStateListener?.onLevelChanged(level) + } + } + + private fun checkCollision(x: Int, y: Int, piece: Array>): Boolean { + for (r in piece.indices) { + for (c in piece[r].indices) { + if (piece[r][c] == 1) { + val boardRow = y + r + val boardCol = x + c + + // Check boundaries + if (boardCol < 0 || boardCol >= COLS || boardRow >= ROWS) { + return true + } + + // Skip check above the board + if (boardRow < 0) continue + + // Check if position already filled + if (board[boardRow][boardCol] != EMPTY) { + return true + } + } + } + } + return false + } + + private fun gameOver() { + isRunning = false + isGameOver = true + gameHandler.removeCallbacks(gameRunnable) + gameStateListener?.onGameOver(score) + } + + private fun getDropInterval(): Long { + // Speed increases with level + return (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong() + } + + // Getters for rendering + fun getBoard(): Array> { + return board + } + + fun getCurrentPiece(): Int { + return currentPiece + } + + fun getCurrentRotation(): Int { + return currentRotation + } + + fun getCurrentX(): Int { + return currentX + } + + fun getCurrentY(): Int { + return currentY + } + + fun getCurrentColor(): String { + return currentColor + } + + fun getNextPiece(): Int { + return nextPiece + } + + fun getNextColor(): String { + return nextColor + } + + fun getCurrentPieceArray(): Array> { + return pieces[currentPiece][currentRotation] + } + + fun getNextPieceArray(): Array> { + return pieces[nextPiece][0] + } + + fun calculateShadowY(): Int { + var shadowY = currentY + + while (!checkCollision(currentX, shadowY + 1, getCurrentPieceArray())) { + shadowY++ + } + + return shadowY + } + + // Get current rotation values for rendering + fun getRotation3DX(): Float = if (isRotating) currentRotation3DX else rotation3DX.toFloat() + fun getRotation3DY(): Float = if (isRotating) currentRotation3DY else rotation3DY.toFloat() + fun isRotating(): Boolean = isRotating +} \ No newline at end of file diff --git a/android-app/app/src/main/java/com/tetris3d/views/NextPieceView.kt b/android-app/app/src/main/java/com/tetris3d/views/NextPieceView.kt new file mode 100644 index 0000000..afbb596 --- /dev/null +++ b/android-app/app/src/main/java/com/tetris3d/views/NextPieceView.kt @@ -0,0 +1,128 @@ +package com.tetris3d.views + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.LinearGradient +import android.graphics.Paint +import android.graphics.RectF +import android.graphics.Shader +import android.util.AttributeSet +import android.view.View +import com.tetris3d.game.TetrisGame + +/** + * Custom view for rendering the next Tetris piece + */ +class NextPieceView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + private var game: TetrisGame? = null + private val paint = Paint() + private var blockSize = 0f + + // Background gradient colors + private val bgColorStart = Color.parseColor("#1a1a2e") + private val bgColorEnd = Color.parseColor("#0f3460") + private lateinit var bgGradient: LinearGradient + + fun setGame(game: TetrisGame) { + this.game = game + invalidate() + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + + // Create background gradient + bgGradient = LinearGradient( + 0f, 0f, w.toFloat(), h.toFloat(), + bgColorStart, bgColorEnd, + Shader.TileMode.CLAMP + ) + + // Determine block size based on the smaller dimension + blockSize = (Math.min(width, height) / 4).toFloat() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + val game = this.game ?: return + + // Draw gradient background + paint.shader = bgGradient + canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint) + paint.shader = null + + // Draw border with glow effect + drawBorder(canvas) + + // Draw the next piece + drawNextPiece(canvas, game) + } + + private fun drawBorder(canvas: Canvas) { + // Draw a glowing border + val borderRect = RectF(2f, 2f, width - 2f, height - 2f) + + // Outer glow (cyan color like in the web app) + paint.style = Paint.Style.STROKE + paint.strokeWidth = 2f + paint.color = Color.parseColor("#00ffff") + paint.setShadowLayer(5f, 0f, 0f, Color.parseColor("#00ffff")) + canvas.drawRect(borderRect, paint) + paint.setShadowLayer(0f, 0f, 0f, 0) + } + + private fun drawNextPiece(canvas: Canvas, game: TetrisGame) { + val piece = game.getNextPieceArray() + val color = game.getNextColor() + + // Center the piece in the view + val offsetX = (width - piece[0].size * blockSize) / 2 + val offsetY = (height - piece.size * blockSize) / 2 + + for (r in piece.indices) { + for (c in piece[r].indices) { + if (piece[r][c] == 1) { + drawBlock(canvas, offsetX + c * blockSize, offsetY + r * blockSize, color) + } + } + } + } + + private fun drawBlock(canvas: Canvas, x: Float, y: Float, colorStr: String) { + val left = x + val top = y + val right = left + blockSize + val bottom = top + blockSize + val blockRect = RectF(left, top, right, bottom) + + // Draw the block fill + paint.style = Paint.Style.FILL + paint.color = Color.parseColor(colorStr) + canvas.drawRect(blockRect, paint) + + // Draw the highlight (top-left gradient) + paint.style = Paint.Style.FILL + val highlightPaint = Paint() + highlightPaint.shader = LinearGradient( + left, top, + right, bottom, + Color.argb(120, 255, 255, 255), + Color.argb(0, 255, 255, 255), + Shader.TileMode.CLAMP + ) + canvas.drawRect(blockRect, highlightPaint) + + // Draw the block border + paint.style = Paint.Style.STROKE + paint.strokeWidth = 2f + paint.color = Color.BLACK + canvas.drawRect(blockRect, paint) + } +} \ No newline at end of file diff --git a/android-app/app/src/main/java/com/tetris3d/views/TetrisGameView.kt b/android-app/app/src/main/java/com/tetris3d/views/TetrisGameView.kt new file mode 100644 index 0000000..f6376bd --- /dev/null +++ b/android-app/app/src/main/java/com/tetris3d/views/TetrisGameView.kt @@ -0,0 +1,460 @@ +package com.tetris3d.views + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.LinearGradient +import android.graphics.Paint +import android.graphics.RectF +import android.graphics.Shader +import android.os.Handler +import android.os.Looper +import android.util.AttributeSet +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.View +import android.view.animation.DecelerateInterpolator +import com.tetris3d.game.TetrisGame +import kotlin.math.abs +import kotlin.math.cos +import kotlin.math.min +import kotlin.math.sin + +/** + * Custom view for rendering the Tetris game board + */ +class TetrisGameView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + private var game: TetrisGame? = null + private val paint = Paint() + private var blockSize = 0f + private var boardLeft = 0f + private var boardTop = 0f + + // Shadow and grid configuration + private val showShadow = true + private val showGrid = true + + // Background gradient colors + private val bgColorStart = Color.parseColor("#1a1a2e") + private val bgColorEnd = Color.parseColor("#0f3460") + private lateinit var bgGradient: LinearGradient + + // Gesture detection for swipe controls + private val gestureDetector = GestureDetector(context, TetrisGestureListener()) + + // Define minimum swipe velocity and distance + private val minSwipeVelocity = 50 // Lowered for better sensitivity + private val minSwipeDistance = 20 // Lowered for better sensitivity + + // Movement control + private val autoRepeatHandler = Handler(Looper.getMainLooper()) + private var isAutoRepeating = false + private var currentMovement: (() -> Unit)? = null + private val autoRepeatDelay = 40L // Faster repeat for smoother movement + private val initialAutoRepeatDelay = 100L // Initial delay before repeating + private val interpolator = DecelerateInterpolator(1.5f) + + // Touch tracking for continuous swipe + private var lastTouchX = 0f + private var lastTouchY = 0f + private var swipeThreshold = 15f // Distance needed to trigger a move while dragging + + // Refresh timer + private val refreshHandler = Handler(Looper.getMainLooper()) + private val refreshRunnable = object : Runnable { + override fun run() { + // Update the game rotation animation + game?.updateRotation() + invalidate() + refreshHandler.postDelayed(this, REFRESH_INTERVAL) + } + } + + companion object { + private const val REFRESH_INTERVAL = 16L // ~60fps + } + + fun setGame(game: TetrisGame) { + this.game = game + invalidate() + + // Start refresh timer + startRefreshTimer() + } + + private fun startRefreshTimer() { + refreshHandler.removeCallbacks(refreshRunnable) + refreshHandler.postDelayed(refreshRunnable, REFRESH_INTERVAL) + } + + private fun stopRefreshTimer() { + refreshHandler.removeCallbacks(refreshRunnable) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + + // Create background gradient + bgGradient = LinearGradient( + 0f, 0f, w.toFloat(), h.toFloat(), + bgColorStart, bgColorEnd, + Shader.TileMode.CLAMP + ) + + // Calculate block size based on available space + val rows = TetrisGame.ROWS + val cols = TetrisGame.COLS + + // Determine the maximum block size that will fit in the view + val maxBlockWidth = width.toFloat() / cols + val maxBlockHeight = height.toFloat() / rows + + // Use the smaller dimension to ensure squares + blockSize = min(maxBlockWidth, maxBlockHeight) + + // Center the board + boardLeft = (width - cols * blockSize) / 2 + boardTop = (height - rows * blockSize) / 2 + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + val game = this.game ?: return + + // Draw gradient background + paint.shader = bgGradient + canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint) + paint.shader = null + + // Draw grid if enabled + if (showGrid) { + drawGrid(canvas) + } + + // Draw board border with glow effect + drawBoardBorder(canvas) + + // Draw the locked pieces on the board + drawBoard(canvas, game) + + // Draw shadow piece if enabled + if (showShadow && !game.isGameOver && game.isRunning) { + drawShadowPiece(canvas, game) + } + + // Draw current active piece with 3D rotation effect + if (!game.isGameOver) { + drawActivePiece(canvas, game) + } + } + + private fun drawBoardBorder(canvas: Canvas) { + // Draw a glowing border around the game board + val borderRect = RectF( + boardLeft - 4f, + boardTop - 4f, + boardLeft + TetrisGame.COLS * blockSize + 4f, + boardTop + TetrisGame.ROWS * blockSize + 4f + ) + + // Outer glow (cyan color like in the web app) + paint.style = Paint.Style.STROKE + paint.strokeWidth = 4f + paint.color = Color.parseColor("#00ffff") + paint.setShadowLayer(8f, 0f, 0f, Color.parseColor("#00ffff")) + canvas.drawRect(borderRect, paint) + paint.setShadowLayer(0f, 0f, 0f, 0) + } + + private fun drawBoard(canvas: Canvas, game: TetrisGame) { + val board = game.getBoard() + + for (r in 0 until TetrisGame.ROWS) { + for (c in 0 until TetrisGame.COLS) { + val color = board[r][c] + if (color != TetrisGame.EMPTY) { + drawBlock(canvas, c, r, color) + } + } + } + } + + private fun drawGrid(canvas: Canvas) { + paint.color = Color.parseColor("#222222") + paint.style = Paint.Style.STROKE + paint.strokeWidth = 1f + + // Draw vertical lines + for (c in 0..TetrisGame.COLS) { + val x = boardLeft + c * blockSize + canvas.drawLine(x, boardTop, x, boardTop + TetrisGame.ROWS * blockSize, paint) + } + + // Draw horizontal lines + for (r in 0..TetrisGame.ROWS) { + val y = boardTop + r * blockSize + canvas.drawLine(boardLeft, y, boardLeft + TetrisGame.COLS * blockSize, y, paint) + } + } + + private fun drawActivePiece(canvas: Canvas, game: TetrisGame) { + val piece = game.getCurrentPieceArray() + val x = game.getCurrentX() + val y = game.getCurrentY() + val color = game.getCurrentColor() + + // Save canvas state for rotation + canvas.save() + + // Get 3D rotation values (0-3 for each axis) + val rotationX = game.getRotation3DX() + val rotationY = game.getRotation3DY() + + // Convert rotation to radians (0-2π) + val angleX = rotationX * Math.PI / 2 + val angleY = rotationY * Math.PI / 2 + + // Calculate center point of the piece for rotation + val centerX = boardLeft + (x + piece[0].size / 2f) * blockSize + val centerY = boardTop + (y + piece.size / 2f) * blockSize + + // Translate to center point, rotate, then translate back + canvas.translate(centerX, centerY) + + // Apply 3D perspective scaling based on rotation angles + val scaleX = cos(angleY.toFloat()).coerceAtLeast(0.5f) + val scaleY = cos(angleX.toFloat()).coerceAtLeast(0.5f) + canvas.scale(scaleX, scaleY) + + // Translate back + canvas.translate(-centerX, -centerY) + + // Draw the piece with perspective + for (r in piece.indices) { + for (c in piece[r].indices) { + if (piece[r][c] == 1) { + // Calculate position with perspective effect + val offsetX = sin(angleY.toFloat()) * blockSize * 0.2f + val offsetY = sin(angleX.toFloat()) * blockSize * 0.2f + + drawBlock( + canvas, + x + c, + y + r, + color, + offsetX = offsetX, + offsetY = offsetY + ) + } + } + } + + // Restore canvas state + canvas.restore() + } + + private fun drawShadowPiece(canvas: Canvas, game: TetrisGame) { + val piece = game.getCurrentPieceArray() + val x = game.getCurrentX() + val y = game.calculateShadowY() + + if (y == game.getCurrentY()) { + return // Skip if shadow is at the same position as the piece + } + + paint.color = Color.parseColor("#444444") + paint.style = Paint.Style.STROKE + paint.strokeWidth = 2f + + for (r in piece.indices) { + for (c in piece[r].indices) { + if (piece[r][c] == 1) { + val left = boardLeft + (x + c) * blockSize + val top = boardTop + (y + r) * blockSize + val right = left + blockSize + val bottom = top + blockSize + canvas.drawRect(left, top, right, bottom, paint) + } + } + } + } + + private fun drawBlock(canvas: Canvas, x: Int, y: Int, colorStr: String, offsetX: Float = 0f, offsetY: Float = 0f) { + // Skip drawing outside the board + if (y < 0) return + + val left = boardLeft + x * blockSize + offsetX + val top = boardTop + y * blockSize + offsetY + val right = left + blockSize + val bottom = top + blockSize + val blockRect = RectF(left, top, right, bottom) + + // Draw the block fill + paint.style = Paint.Style.FILL + paint.color = Color.parseColor(colorStr) + canvas.drawRect(blockRect, paint) + + // Draw the highlight (top-left gradient) + paint.style = Paint.Style.FILL + val highlightPaint = Paint() + highlightPaint.shader = LinearGradient( + left, top, + right, bottom, + Color.argb(120, 255, 255, 255), + Color.argb(0, 255, 255, 255), + Shader.TileMode.CLAMP + ) + canvas.drawRect(blockRect, highlightPaint) + + // Draw the block border + paint.style = Paint.Style.STROKE + paint.strokeWidth = 2f + paint.color = Color.BLACK + canvas.drawRect(blockRect, paint) + } + + // Handler for touch events + override fun onTouchEvent(event: MotionEvent): Boolean { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + lastTouchX = event.x + lastTouchY = event.y + } + MotionEvent.ACTION_MOVE -> { + val diffX = event.x - lastTouchX + val diffY = event.y - lastTouchY + + // Check if drag distance exceeds threshold for continuous movement + if (abs(diffX) > swipeThreshold && abs(diffX) > abs(diffY)) { + // Horizontal continuous movement + if (diffX > 0) { + game?.moveRight() + } else { + game?.moveLeft() + } + // Update last position after processing the move + lastTouchX = event.x + lastTouchY = event.y + invalidate() + return true + } else if (abs(diffY) > swipeThreshold && abs(diffY) > abs(diffX)) { + // Vertical continuous movement - only for downward + if (diffY > 0) { + game?.moveDown() + // Update last position after processing the move + lastTouchX = event.x + lastTouchY = event.y + invalidate() + return true + } + } + } + MotionEvent.ACTION_UP -> { + stopAutoRepeat() + } + } + + return gestureDetector.onTouchEvent(event) || super.onTouchEvent(event) + } + + private fun startAutoRepeat(action: () -> Unit) { + isAutoRepeating = true + currentMovement = action + + val autoRepeatRunnable = object : Runnable { + override fun run() { + if (isAutoRepeating && currentMovement != null) { + currentMovement?.invoke() + invalidate() + autoRepeatHandler.postDelayed(this, autoRepeatDelay) + } + } + } + + // Use initial delay before first repeat + autoRepeatHandler.postDelayed(autoRepeatRunnable, initialAutoRepeatDelay) + } + + private fun stopAutoRepeat() { + isAutoRepeating = false + currentMovement = null + autoRepeatHandler.removeCallbacksAndMessages(null) + } + + // Clean up refresh timer + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + stopRefreshTimer() + stopAutoRepeat() + } + + // Gesture listener for swipe controls + inner class TetrisGestureListener : GestureDetector.SimpleOnGestureListener() { + + override fun onDown(e: MotionEvent): Boolean { + return true + } + + override fun onSingleTapUp(e: MotionEvent): Boolean { + // Determine if tap is on left or right side of screen + val screenMiddle = width / 2 + + if (e.x < screenMiddle) { + // Left side - rotate counterclockwise (in a real 3D game) + game?.rotate3DX() + } else { + // Right side - rotate clockwise + game?.rotate3DY() + } + + invalidate() + return true + } + + override fun onFling( + e1: MotionEvent, + e2: MotionEvent, + velocityX: Float, + velocityY: Float + ): Boolean { + val diffX = e2.x - e1.x + val diffY = e2.y - e1.y + + // Check if swipe is horizontal or vertical based on magnitude + if (abs(diffX) > abs(diffY)) { + // Horizontal swipe + if (abs(velocityX) > minSwipeVelocity && abs(diffX) > minSwipeDistance) { + if (diffX > 0) { + // Swipe right - use auto-repeat for smoother movement + startAutoRepeat { game?.moveRight() } + } else { + // Swipe left - use auto-repeat for smoother movement + startAutoRepeat { game?.moveLeft() } + } + return true + } + } else { + // Vertical swipe + if (abs(velocityY) > minSwipeVelocity && abs(diffY) > minSwipeDistance) { + if (diffY > 0) { + // Swipe down - start soft drop with auto-repeat + startAutoRepeat { game?.moveDown() } + } else { + // Swipe up - hard drop + game?.hardDrop() + invalidate() + } + return true + } + } + + return false + } + } +} \ No newline at end of file diff --git a/android-app/app/src/main/res/drawable/ic_launcher_background.xml b/android-app/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..a4fb788 --- /dev/null +++ b/android-app/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/drawable/ic_launcher_foreground.xml b/android-app/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..fcb9246 --- /dev/null +++ b/android-app/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/drawable/simple_icon.xml b/android-app/app/src/main/res/drawable/simple_icon.xml new file mode 100644 index 0000000..c4989f1 --- /dev/null +++ b/android-app/app/src/main/res/drawable/simple_icon.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/layout/activity_main.xml b/android-app/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..964f9f0 --- /dev/null +++ b/android-app/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/layout/dialog_game_over.xml b/android-app/app/src/main/res/layout/dialog_game_over.xml new file mode 100644 index 0000000..ebf407c --- /dev/null +++ b/android-app/app/src/main/res/layout/dialog_game_over.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/layout/dialog_options.xml b/android-app/app/src/main/res/layout/dialog_options.xml new file mode 100644 index 0000000..6529074 --- /dev/null +++ b/android-app/app/src/main/res/layout/dialog_options.xml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..d58b8f1 --- /dev/null +++ b/android-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..d58b8f1 --- /dev/null +++ b/android-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/mipmap-hdpi/ic_launcher.xml b/android-app/app/src/main/res/mipmap-hdpi/ic_launcher.xml new file mode 100644 index 0000000..4f7aee8 --- /dev/null +++ b/android-app/app/src/main/res/mipmap-hdpi/ic_launcher.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/mipmap-hdpi/ic_launcher_round.xml b/android-app/app/src/main/res/mipmap-hdpi/ic_launcher_round.xml new file mode 100644 index 0000000..4f7aee8 --- /dev/null +++ b/android-app/app/src/main/res/mipmap-hdpi/ic_launcher_round.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/mipmap-mdpi/ic_launcher.xml b/android-app/app/src/main/res/mipmap-mdpi/ic_launcher.xml new file mode 100644 index 0000000..2299249 --- /dev/null +++ b/android-app/app/src/main/res/mipmap-mdpi/ic_launcher.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/mipmap-mdpi/ic_launcher_round.xml b/android-app/app/src/main/res/mipmap-mdpi/ic_launcher_round.xml new file mode 100644 index 0000000..2299249 --- /dev/null +++ b/android-app/app/src/main/res/mipmap-mdpi/ic_launcher_round.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/mipmap-xhdpi/ic_launcher.xml b/android-app/app/src/main/res/mipmap-xhdpi/ic_launcher.xml new file mode 100644 index 0000000..dfcd653 --- /dev/null +++ b/android-app/app/src/main/res/mipmap-xhdpi/ic_launcher.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.xml b/android-app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.xml new file mode 100644 index 0000000..dfcd653 --- /dev/null +++ b/android-app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/mipmap-xxhdpi/ic_launcher.xml b/android-app/app/src/main/res/mipmap-xxhdpi/ic_launcher.xml new file mode 100644 index 0000000..3e5bb1d --- /dev/null +++ b/android-app/app/src/main/res/mipmap-xxhdpi/ic_launcher.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.xml b/android-app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.xml new file mode 100644 index 0000000..3e5bb1d --- /dev/null +++ b/android-app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.xml b/android-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.xml new file mode 100644 index 0000000..c4a2986 --- /dev/null +++ b/android-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.xml b/android-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.xml new file mode 100644 index 0000000..c4a2986 --- /dev/null +++ b/android-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/android-app/app/src/main/res/values/colors.xml b/android-app/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..c2d02d1 --- /dev/null +++ b/android-app/app/src/main/res/values/colors.xml @@ -0,0 +1,15 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + #FF00FFFF + #FF00AAAA + #FFFF00FF + #FFAA00AA + #FF000020 + \ No newline at end of file diff --git a/android-app/app/src/main/res/values/strings.xml b/android-app/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..833398e --- /dev/null +++ b/android-app/app/src/main/res/values/strings.xml @@ -0,0 +1,19 @@ + + 3D Tetris + Score + Lines + Level + Pause + Start + Game Over + Final Score + Play Again + Shadow + Options + 3D Effects + Spin Animations + Animation Speed + Starting Level + Apply + Close + \ No newline at end of file diff --git a/android-app/app/src/main/res/values/themes.xml b/android-app/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..05d2ed1 --- /dev/null +++ b/android-app/app/src/main/res/values/themes.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/android-app/build.gradle b/android-app/build.gradle new file mode 100644 index 0000000..3f86daa --- /dev/null +++ b/android-app/build.gradle @@ -0,0 +1,22 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.9.0' + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0' + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/android-app/build/reports/problems/problems-report.html b/android-app/build/reports/problems/problems-report.html new file mode 100644 index 0000000..cb3e8f3 --- /dev/null +++ b/android-app/build/reports/problems/problems-report.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + Gradle Configuration Cache + + + + + + + Loading... + + + + + + + diff --git a/android-app/gradle.properties b/android-app/gradle.properties new file mode 100644 index 0000000..a84c3d0 --- /dev/null +++ b/android-app/gradle.properties @@ -0,0 +1,19 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# 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 +android.useAndroidX=true + +# 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 + +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official + +android.defaults.buildfeatures.buildconfig=true +android.nonFinalResIds=false \ No newline at end of file diff --git a/android-app/gradle/wrapper/gradle-wrapper.jar b/android-app/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..a4b76b9 Binary files /dev/null and b/android-app/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android-app/gradle/wrapper/gradle-wrapper.properties b/android-app/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e2847c8 --- /dev/null +++ b/android-app/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/android-app/gradlew b/android-app/gradlew new file mode 100755 index 0000000..f5feea6 --- /dev/null +++ b/android-app/gradlew @@ -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" "$@" diff --git a/android-app/gradlew.bat b/android-app/gradlew.bat new file mode 100644 index 0000000..9b42019 --- /dev/null +++ b/android-app/gradlew.bat @@ -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 diff --git a/android-app/local.properties b/android-app/local.properties new file mode 100644 index 0000000..5ef8021 --- /dev/null +++ b/android-app/local.properties @@ -0,0 +1,8 @@ +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +#Tue Mar 25 23:36:25 EDT 2025 +sdk.dir=/home/corey/Android/Sdk diff --git a/android-app/settings.gradle b/android-app/settings.gradle new file mode 100644 index 0000000..9781148 --- /dev/null +++ b/android-app/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = "Tetris3D" +include ':app' \ No newline at end of file diff --git a/index.html b/index.html index 58f8758..a3ab14c 100644 --- a/index.html +++ b/index.html @@ -2,9 +2,7 @@ - - - + 3D Tetris @@ -80,53 +78,41 @@ - Game Options + Options - Enable 3D Effects - + 3D Effects + + + + + Enable 3D block rendering for a more immersive experience - Enable Spin Animations - + Spin Animations + + + + + Show 3D rotation animations when rotating pieces Animation Speed - - - - Force Mobile Controls - - - - - Performance Mode - - Reduces visual effects for better performance on mobile devices + Adjust the speed of 3D animations Starting Level - - Level 1 - Level 2 - Level 3 - Level 4 - Level 5 - Level 6 - Level 7 - Level 8 - Level 9 - Level 10 - + + Set the level you want to start at (1-10) - Apply Close + Apply diff --git a/script.js b/script.js index 800b148..d44a24a 100644 --- a/script.js +++ b/script.js @@ -2072,68 +2072,17 @@ function handleResize() { const gameWrapper = document.querySelector('.game-wrapper'); const scoreContainer = document.querySelector('.score-container'); - if (isMobile || forceMobileControls) { - // Scale the canvas to fit mobile screen - const viewportWidth = Math.min(window.innerWidth, document.documentElement.clientWidth); - const viewportHeight = Math.min(window.innerHeight, document.documentElement.clientHeight); - - // Detect orientation - const isPortrait = viewportHeight > viewportWidth; - - // Calculate available game area (accounting for UI elements) - const titleHeight = 40; // Estimate for title - const scoreWidth = isPortrait ? 120 : 100; // Width for score container in portrait/landscape - const availableWidth = viewportWidth - scoreWidth - 20; // Subtract score width + padding - const availableHeight = viewportHeight - titleHeight - 20; // Subtract title height + padding - - // Calculate optimal dimensions while maintaining aspect ratio - const gameRatio = ROWS / COLS; - - // Calculate scale based on available space - let targetWidth, targetHeight; - - if (isPortrait) { - // For portrait, prioritize fitting the width - targetWidth = availableWidth * 0.95; - targetHeight = targetWidth * gameRatio; - - // If too tall, scale down based on height - if (targetHeight > availableHeight * 0.95) { - targetHeight = availableHeight * 0.95; - targetWidth = targetHeight / gameRatio; - } - } else { - // For landscape, prioritize fitting the height - targetHeight = availableHeight * 0.95; - targetWidth = targetHeight / gameRatio; - - // If too wide, scale down based on width - if (targetWidth > availableWidth * 0.95) { - targetWidth = availableWidth * 0.95; - targetHeight = targetWidth * gameRatio; - } + // Make sure canvas maintains its dimensions + canvas.width = COLS * BLOCK_SIZE; + canvas.height = ROWS * BLOCK_SIZE; + + // Redraw the board and current piece + drawBoard(); + if (p) { + p.draw(); + if (showShadow) { + p.drawShadow(); } - - // Apply dimensions - canvas.style.width = `${targetWidth}px`; - canvas.style.height = `${targetHeight}px`; - - // Force a redraw of the nextPiece preview to fix rendering issues - if (nextPiece) { - setTimeout(() => { - nextPieceCtx.clearRect(0, 0, nextPieceCanvas.width, nextPieceCanvas.height); - nextPiece.drawNextPiece(); - }, 100); - } - - // Show touch controls mode - document.body.classList.add('mobile-mode'); - } else { - // Reset to desktop layout - canvas.style.width = ''; - canvas.style.height = ''; - - document.body.classList.remove('mobile-mode'); } } diff --git a/style.css b/style.css index 96b4f96..bb84ba2 100644 --- a/style.css +++ b/style.css @@ -404,16 +404,11 @@ canvas#tetris { max-width: 200px; } +/* Mobile-only class should be hidden always since we're removing mobile support */ .mobile-only { display: none; } -@media (max-width: 768px) { - .mobile-only { - display: flex; - } -} - /* Toggle switch styles */ .switch { position: relative; @@ -504,14 +499,7 @@ input[type=range]::-moz-range-thumb { box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); } -/* Mobile-specific styles */ -.mobile-mode { - overflow: hidden; - overscroll-behavior: none; - touch-action: none; -} - -/* 3D Rotation buttons */ +/* 3D Rotation buttons - keep these for desktop */ .rotate-buttons { position: fixed; bottom: 20px; @@ -541,257 +529,10 @@ input[type=range]::-moz-range-thumb { background: linear-gradient(45deg, #0062cc, #00b3ff); } -/* Performance toggle */ -.perf-toggle { - display: flex; - align-items: center; - margin-top: 5px; - background: rgba(0, 0, 0, 0.3); - padding: 5px; - border-radius: 4px; - font-size: 11px; -} - -.perf-label { - margin-left: 5px; - color: #00ffff; - cursor: pointer; -} - -#toggle-mobile-performance { - cursor: pointer; -} - -/* Mobile mode adjustments for rotation buttons */ -.mobile-mode .rotate-buttons { - bottom: 15px; - right: 15px; - gap: 8px; -} - -.mobile-mode .rotate-btn { - padding: 10px 12px; - font-size: 12px; -} - -/* Mobile landscape orientation */ -@media (orientation: landscape) { - .mobile-mode .rotate-buttons { - flex-direction: row; - bottom: 10px; - right: 10px; - } -} - -/* Smaller devices */ -@media (max-width: 400px) { - .mobile-mode .rotate-btn { - padding: 8px 10px; - font-size: 11px; - } -} - -.mobile-mode .game-container { - flex-direction: row; - align-items: flex-start; - gap: 5px; - padding: 10px; - max-width: 100vw; - box-sizing: border-box; - margin-top: 40px; /* Reduced top margin */ -} - -.mobile-mode .game-title { - font-size: 22px; - top: 5px; - text-shadow: 0 0 8px rgba(255, 0, 255, 0.7); -} - -.mobile-mode .game-wrapper { - display: flex; - flex-direction: column; - align-items: center; - margin: 0; - order: 2; /* Move game board to the right */ - flex-grow: 1; -} - -.mobile-mode .score-container { - width: auto; - min-width: 100px; - max-width: 120px; - display: flex; - flex-direction: column; - align-items: center; - gap: 5px; - padding: 8px; - background: rgba(0, 0, 0, 0.7); - order: 1; /* Move score container to the left */ - margin-right: 5px; - border-radius: 8px; - height: auto; - align-self: stretch; -} - -.mobile-mode .score-container p { - margin: 3px 0; - font-size: 10px; -} - -.mobile-mode .game-btn { - margin: 3px; - padding: 6px 8px; - font-size: 9px; - width: 95%; -} - -.mobile-mode .controls-info { - display: none; -} - -.mobile-mode #next-piece-preview { - position: relative; - top: auto; - bottom: auto; - left: auto; - right: auto; - transform: none; - margin: 0 0 10px 0; - background: rgba(0, 0, 0, 0.7); - z-index: 10; - border-color: rgba(0, 255, 255, 0.5); - width: 90%; /* Make it fit inside the score container */ - box-sizing: border-box; -} - -.mobile-mode #next-piece-preview h3 { - font-size: 12px; - margin-bottom: 5px; -} - -.mobile-mode canvas#tetris { - max-width: 100%; - height: auto; - border-radius: 8px; -} - -/* Portrait vs landscape adjustments */ -@media (orientation: portrait) { - .mobile-mode .game-container { - padding-top: 35px; - height: calc(100vh - 40px); - } - - .mobile-mode .score-container { - height: calc(100vh - 80px); - justify-content: flex-start; - } - - .mobile-mode canvas#tetris { - height: calc(100vh - 80px); - width: auto; - object-fit: contain; - } -} - -@media (orientation: landscape) { - .mobile-mode .game-container { - margin-top: 35px; - padding: 5px; - height: calc(100vh - 40px); - } - - .mobile-mode .score-container { - height: calc(100vh - 50px); - max-width: 100px; - } - - .mobile-mode canvas#tetris { - height: calc(100vh - 50px); - width: auto; - object-fit: contain; - } -} - -/* Smaller devices */ -@media (max-width: 400px) { - .mobile-mode .game-title { - font-size: 18px; - } - - .mobile-mode .score-container p { - font-size: 9px; - } - - .mobile-mode .score-container { - min-width: 85px; - max-width: 90px; - } - - .mobile-mode .game-btn { - font-size: 8px; - padding: 5px 6px; - } -} - -/* Tablet optimization */ -@media (min-width: 768px) and (max-width: 1024px) { - .mobile-mode .game-container { - flex-direction: row; - flex-wrap: wrap; - gap: 20px; - } - - .mobile-mode .game-title { - font-size: 28px; - } -} - -/* Touch instructions */ -.touch-instructions { - display: none; - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: rgba(0, 0, 0, 0.9); - border: 2px solid rgba(0, 255, 255, 0.7); - border-radius: 10px; - padding: 15px; - z-index: 1000; - box-shadow: 0 0 20px rgba(0, 255, 255, 0.5); - transition: opacity 1s ease; - text-align: center; - max-width: 90vw; -} - -.touch-instructions h3 { - color: #00ffff; - text-shadow: 0 0 5px #00ffff, 0 0 10px #00ffff; - margin-bottom: 15px; - font-size: 16px; -} - -.mobile-mode .touch-instructions { - display: block; -} - -.touch-instructions p { - margin: 10px 0; - font-size: 13px; - color: #fff; - text-align: left; -} - -.touch-instructions p b { - color: #ff9900; - text-shadow: 0 0 5px rgba(255, 153, 0, 0.5); -} - -.touch-instructions.fade-out { - opacity: 0; -} +/* Performance toggle - Since we're removing mobile support */ +/* Removing mobile-specific media queries and styles */ +/* Removing touch-instructions class and related styles */ .instructions-btn { margin-top: 15px; background: linear-gradient(45deg, #ff9900, #ff5500);