diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index c5f3f6b..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "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 deleted file mode 100644 index 4aeb5ce..0000000 Binary files a/android-app/.gradle/7.5/checksums/checksums.lock and /dev/null differ diff --git a/android-app/.gradle/7.5/fileChanges/last-build.bin b/android-app/.gradle/7.5/fileChanges/last-build.bin deleted file mode 100644 index f76dd23..0000000 Binary files a/android-app/.gradle/7.5/fileChanges/last-build.bin and /dev/null differ diff --git a/android-app/.gradle/7.5/fileHashes/fileHashes.lock b/android-app/.gradle/7.5/fileHashes/fileHashes.lock deleted file mode 100644 index 2411039..0000000 Binary files a/android-app/.gradle/7.5/fileHashes/fileHashes.lock and /dev/null differ diff --git a/android-app/.gradle/7.5/gc.properties b/android-app/.gradle/7.5/gc.properties deleted file mode 100644 index e69de29..0000000 diff --git a/android-app/.gradle/8.10/checksums/checksums.lock b/android-app/.gradle/8.10/checksums/checksums.lock deleted file mode 100644 index 9bcb542..0000000 Binary files a/android-app/.gradle/8.10/checksums/checksums.lock and /dev/null differ diff --git a/android-app/.gradle/8.10/checksums/md5-checksums.bin b/android-app/.gradle/8.10/checksums/md5-checksums.bin deleted file mode 100644 index 509ec20..0000000 Binary files a/android-app/.gradle/8.10/checksums/md5-checksums.bin and /dev/null differ diff --git a/android-app/.gradle/8.10/checksums/sha1-checksums.bin b/android-app/.gradle/8.10/checksums/sha1-checksums.bin deleted file mode 100644 index 6bd57f3..0000000 Binary files a/android-app/.gradle/8.10/checksums/sha1-checksums.bin and /dev/null differ diff --git a/android-app/.gradle/8.10/dependencies-accessors/gc.properties b/android-app/.gradle/8.10/dependencies-accessors/gc.properties deleted file mode 100644 index e69de29..0000000 diff --git a/android-app/.gradle/8.10/executionHistory/executionHistory.bin b/android-app/.gradle/8.10/executionHistory/executionHistory.bin deleted file mode 100644 index 1e5933b..0000000 Binary files a/android-app/.gradle/8.10/executionHistory/executionHistory.bin and /dev/null differ diff --git a/android-app/.gradle/8.10/executionHistory/executionHistory.lock b/android-app/.gradle/8.10/executionHistory/executionHistory.lock deleted file mode 100644 index d420e60..0000000 Binary files a/android-app/.gradle/8.10/executionHistory/executionHistory.lock and /dev/null differ diff --git a/android-app/.gradle/8.10/fileChanges/last-build.bin b/android-app/.gradle/8.10/fileChanges/last-build.bin deleted file mode 100644 index f76dd23..0000000 Binary files a/android-app/.gradle/8.10/fileChanges/last-build.bin and /dev/null differ diff --git a/android-app/.gradle/8.10/fileHashes/fileHashes.bin b/android-app/.gradle/8.10/fileHashes/fileHashes.bin deleted file mode 100644 index 5fd80eb..0000000 Binary files a/android-app/.gradle/8.10/fileHashes/fileHashes.bin and /dev/null differ diff --git a/android-app/.gradle/8.10/fileHashes/fileHashes.lock b/android-app/.gradle/8.10/fileHashes/fileHashes.lock deleted file mode 100644 index eaa5424..0000000 Binary files a/android-app/.gradle/8.10/fileHashes/fileHashes.lock and /dev/null differ diff --git a/android-app/.gradle/8.10/gc.properties b/android-app/.gradle/8.10/gc.properties deleted file mode 100644 index e69de29..0000000 diff --git a/android-app/.gradle/8.11.1/checksums/checksums.lock b/android-app/.gradle/8.11.1/checksums/checksums.lock deleted file mode 100644 index 3a2f1da..0000000 Binary files a/android-app/.gradle/8.11.1/checksums/checksums.lock and /dev/null 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 deleted file mode 100644 index 9f7f27d..0000000 Binary files a/android-app/.gradle/8.11.1/checksums/md5-checksums.bin and /dev/null 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 deleted file mode 100644 index 27b3854..0000000 Binary files a/android-app/.gradle/8.11.1/checksums/sha1-checksums.bin and /dev/null differ diff --git a/android-app/.gradle/8.11.1/executionHistory/executionHistory.bin b/android-app/.gradle/8.11.1/executionHistory/executionHistory.bin deleted file mode 100644 index 1e65897..0000000 Binary files a/android-app/.gradle/8.11.1/executionHistory/executionHistory.bin and /dev/null differ diff --git a/android-app/.gradle/8.11.1/executionHistory/executionHistory.lock b/android-app/.gradle/8.11.1/executionHistory/executionHistory.lock deleted file mode 100644 index 152df37..0000000 Binary files a/android-app/.gradle/8.11.1/executionHistory/executionHistory.lock and /dev/null 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 deleted file mode 100644 index f76dd23..0000000 Binary files a/android-app/.gradle/8.11.1/fileChanges/last-build.bin and /dev/null differ diff --git a/android-app/.gradle/8.11.1/fileHashes/fileHashes.bin b/android-app/.gradle/8.11.1/fileHashes/fileHashes.bin deleted file mode 100644 index 9dde153..0000000 Binary files a/android-app/.gradle/8.11.1/fileHashes/fileHashes.bin and /dev/null differ diff --git a/android-app/.gradle/8.11.1/fileHashes/fileHashes.lock b/android-app/.gradle/8.11.1/fileHashes/fileHashes.lock deleted file mode 100644 index 8190865..0000000 Binary files a/android-app/.gradle/8.11.1/fileHashes/fileHashes.lock and /dev/null differ diff --git a/android-app/.gradle/8.11.1/fileHashes/resourceHashesCache.bin b/android-app/.gradle/8.11.1/fileHashes/resourceHashesCache.bin deleted file mode 100644 index 92a1969..0000000 Binary files a/android-app/.gradle/8.11.1/fileHashes/resourceHashesCache.bin and /dev/null differ diff --git a/android-app/.gradle/8.11.1/gc.properties b/android-app/.gradle/8.11.1/gc.properties deleted file mode 100644 index e69de29..0000000 diff --git a/android-app/.gradle/8.8/checksums/checksums.lock b/android-app/.gradle/8.8/checksums/checksums.lock deleted file mode 100644 index 7f0b8e9..0000000 Binary files a/android-app/.gradle/8.8/checksums/checksums.lock and /dev/null differ diff --git a/android-app/.gradle/8.8/checksums/md5-checksums.bin b/android-app/.gradle/8.8/checksums/md5-checksums.bin deleted file mode 100644 index efd3160..0000000 Binary files a/android-app/.gradle/8.8/checksums/md5-checksums.bin and /dev/null differ diff --git a/android-app/.gradle/8.8/checksums/sha1-checksums.bin b/android-app/.gradle/8.8/checksums/sha1-checksums.bin deleted file mode 100644 index 0c5c37d..0000000 Binary files a/android-app/.gradle/8.8/checksums/sha1-checksums.bin and /dev/null differ diff --git a/android-app/.gradle/8.8/dependencies-accessors/gc.properties b/android-app/.gradle/8.8/dependencies-accessors/gc.properties deleted file mode 100644 index e69de29..0000000 diff --git a/android-app/.gradle/8.8/fileChanges/last-build.bin b/android-app/.gradle/8.8/fileChanges/last-build.bin deleted file mode 100644 index f76dd23..0000000 Binary files a/android-app/.gradle/8.8/fileChanges/last-build.bin and /dev/null differ diff --git a/android-app/.gradle/8.8/fileHashes/fileHashes.bin b/android-app/.gradle/8.8/fileHashes/fileHashes.bin deleted file mode 100644 index 689e596..0000000 Binary files a/android-app/.gradle/8.8/fileHashes/fileHashes.bin and /dev/null differ diff --git a/android-app/.gradle/8.8/fileHashes/fileHashes.lock b/android-app/.gradle/8.8/fileHashes/fileHashes.lock deleted file mode 100644 index 28f50b2..0000000 Binary files a/android-app/.gradle/8.8/fileHashes/fileHashes.lock and /dev/null differ diff --git a/android-app/.gradle/8.8/gc.properties b/android-app/.gradle/8.8/gc.properties deleted file mode 100644 index e69de29..0000000 diff --git a/android-app/.gradle/8.9/checksums/checksums.lock b/android-app/.gradle/8.9/checksums/checksums.lock deleted file mode 100644 index 347b45b..0000000 Binary files a/android-app/.gradle/8.9/checksums/checksums.lock and /dev/null differ diff --git a/android-app/.gradle/8.9/dependencies-accessors/gc.properties b/android-app/.gradle/8.9/dependencies-accessors/gc.properties deleted file mode 100644 index e69de29..0000000 diff --git a/android-app/.gradle/8.9/executionHistory/executionHistory.lock b/android-app/.gradle/8.9/executionHistory/executionHistory.lock deleted file mode 100644 index 1716945..0000000 Binary files a/android-app/.gradle/8.9/executionHistory/executionHistory.lock and /dev/null differ diff --git a/android-app/.gradle/8.9/fileChanges/last-build.bin b/android-app/.gradle/8.9/fileChanges/last-build.bin deleted file mode 100644 index f76dd23..0000000 Binary files a/android-app/.gradle/8.9/fileChanges/last-build.bin and /dev/null differ diff --git a/android-app/.gradle/8.9/fileHashes/fileHashes.lock b/android-app/.gradle/8.9/fileHashes/fileHashes.lock deleted file mode 100644 index bbe8235..0000000 Binary files a/android-app/.gradle/8.9/fileHashes/fileHashes.lock and /dev/null differ diff --git a/android-app/.gradle/8.9/gc.properties b/android-app/.gradle/8.9/gc.properties deleted file mode 100644 index e69de29..0000000 diff --git a/android-app/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/android-app/.gradle/buildOutputCleanup/buildOutputCleanup.lock deleted file mode 100644 index 8909f05..0000000 Binary files a/android-app/.gradle/buildOutputCleanup/buildOutputCleanup.lock and /dev/null differ diff --git a/android-app/.gradle/buildOutputCleanup/cache.properties b/android-app/.gradle/buildOutputCleanup/cache.properties deleted file mode 100644 index 86e9063..0000000 --- a/android-app/.gradle/buildOutputCleanup/cache.properties +++ /dev/null @@ -1,2 +0,0 @@ -#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 deleted file mode 100644 index 86fc029..0000000 Binary files a/android-app/.gradle/buildOutputCleanup/outputFiles.bin and /dev/null differ diff --git a/android-app/.gradle/config.properties b/android-app/.gradle/config.properties deleted file mode 100644 index f2e3794..0000000 --- a/android-app/.gradle/config.properties +++ /dev/null @@ -1,2 +0,0 @@ -#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 deleted file mode 100644 index c7a2e14..0000000 Binary files a/android-app/.gradle/file-system.probe and /dev/null 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 deleted file mode 100644 index 5c2e123..0000000 Binary files a/android-app/.gradle/nb-cache/android-app-623140067/project-info.ser and /dev/null 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 deleted file mode 100644 index 5de0ac0..0000000 Binary files a/android-app/.gradle/nb-cache/app-271321845/project-info.ser and /dev/null differ diff --git a/android-app/.gradle/nb-cache/subprojects.ser b/android-app/.gradle/nb-cache/subprojects.ser deleted file mode 100644 index faa7cf7..0000000 Binary files a/android-app/.gradle/nb-cache/subprojects.ser and /dev/null differ diff --git a/android-app/.gradle/nb-cache/trust/388F7AB02DFD0183E3FD2FB1B5B4206B0C7407CAFFD7F76F3C1EF65077068EC5 b/android-app/.gradle/nb-cache/trust/388F7AB02DFD0183E3FD2FB1B5B4206B0C7407CAFFD7F76F3C1EF65077068EC5 deleted file mode 100644 index f62917f..0000000 --- a/android-app/.gradle/nb-cache/trust/388F7AB02DFD0183E3FD2FB1B5B4206B0C7407CAFFD7F76F3C1EF65077068EC5 +++ /dev/null @@ -1 +0,0 @@ -8B64E587393F94313EFF41233F48D4848E985369BD2E4B833B25B3765823A6DB diff --git a/android-app/.gradle/vcs-1/gc.properties b/android-app/.gradle/vcs-1/gc.properties deleted file mode 100644 index e69de29..0000000 diff --git a/android-app/.idea/.gitignore b/android-app/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/android-app/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/android-app/.idea/.name b/android-app/.idea/.name deleted file mode 100644 index 8f54013..0000000 --- a/android-app/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -Tetris3D \ No newline at end of file diff --git a/android-app/.idea/AndroidProjectSystem.xml b/android-app/.idea/AndroidProjectSystem.xml deleted file mode 100644 index 4a53bee..0000000 --- a/android-app/.idea/AndroidProjectSystem.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/android-app/.idea/appInsightsSettings.xml b/android-app/.idea/appInsightsSettings.xml deleted file mode 100644 index 371f2e2..0000000 --- a/android-app/.idea/appInsightsSettings.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android-app/.idea/caches/deviceStreaming.xml b/android-app/.idea/caches/deviceStreaming.xml deleted file mode 100644 index 9e9ba09..0000000 --- a/android-app/.idea/caches/deviceStreaming.xml +++ /dev/null @@ -1,607 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android-app/.idea/compiler.xml b/android-app/.idea/compiler.xml deleted file mode 100644 index b86273d..0000000 --- a/android-app/.idea/compiler.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/android-app/.idea/deploymentTargetSelector.xml b/android-app/.idea/deploymentTargetSelector.xml deleted file mode 100644 index b268ef3..0000000 --- a/android-app/.idea/deploymentTargetSelector.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/android-app/.idea/gradle.xml b/android-app/.idea/gradle.xml deleted file mode 100644 index 639c779..0000000 --- a/android-app/.idea/gradle.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android-app/.idea/kotlinc.xml b/android-app/.idea/kotlinc.xml deleted file mode 100644 index 2b8a50f..0000000 --- a/android-app/.idea/kotlinc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/android-app/.idea/migrations.xml b/android-app/.idea/migrations.xml deleted file mode 100644 index f8051a6..0000000 --- a/android-app/.idea/migrations.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/android-app/.idea/misc.xml b/android-app/.idea/misc.xml deleted file mode 100644 index 74dd639..0000000 --- a/android-app/.idea/misc.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/android-app/.idea/runConfigurations.xml b/android-app/.idea/runConfigurations.xml deleted file mode 100644 index 16660f1..0000000 --- a/android-app/.idea/runConfigurations.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android-app/.idea/vcs.xml b/android-app/.idea/vcs.xml deleted file mode 100644 index 6c0b863..0000000 --- a/android-app/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/android-app/README.md b/android-app/README.md deleted file mode 100644 index b12cb8b..0000000 --- a/android-app/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# 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 deleted file mode 100644 index dd59f99..0000000 --- a/android-app/app/build.gradle +++ /dev/null @@ -1,50 +0,0 @@ -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 deleted file mode 100644 index 3f8982d..0000000 --- a/android-app/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - diff --git a/android-app/app/src/main/java/com/tetris3d/MainActivity.kt b/android-app/app/src/main/java/com/tetris3d/MainActivity.kt deleted file mode 100644 index 634ac69..0000000 --- a/android-app/app/src/main/java/com/tetris3d/MainActivity.kt +++ /dev/null @@ -1,254 +0,0 @@ -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 deleted file mode 100644 index 30ba2c7..0000000 --- a/android-app/app/src/main/java/com/tetris3d/game/GameOptions.kt +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index dcf32c6..0000000 --- a/android-app/app/src/main/java/com/tetris3d/game/TetrisGame.kt +++ /dev/null @@ -1,967 +0,0 @@ -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 - - // Refresh interval for animations - const val REFRESH_INTERVAL = 16L // ~60fps - } - - // 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() - - // If a line clear effect is in progress, wait for it to complete - // The view will handle updating and completing the animation - if (lineClearEffect) { - gameHandler.postDelayed(this, REFRESH_INTERVAL) - return - } - - // Move the current piece down - if (!moveDown()) { - // If can't move down, lock the piece - lockPiece() - clearRows() - - // If a line clear effect started, wait for next frame - if (lineClearEffect) { - gameHandler.postDelayed(this, REFRESH_INTERVAL) - return - } - - // Otherwise, continue with creating a new piece - 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 line clear animation started, return and let the game loop handle it - if (lineClearEffect) { - return true - } - - 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 flip the piece vertically - 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 mirror the piece vertically - if (rotation3DX % (maxRotation3D / 2) == 1) { - // Create a vertically mirrored version of the current piece - val currentPattern = getCurrentPieceArray() - val rows = currentPattern.size - val cols = if (rows > 0) currentPattern[0].size else 0 - val mirroredPattern = Array(rows) { r -> Array(cols) { c -> currentPattern[rows - 1 - r][c] } } - - // Check if the mirrored position is valid - if (!checkCollision(currentX, currentY, mirroredPattern)) { - // Replace the current rotation with the mirrored pattern - // Since we don't actually modify the pieces, simulate this by finding a rotation - // that most closely resembles the mirrored pattern, if one exists - - // For symmetrical pieces like O, this may not change anything - val pieceVariants = pieces[currentPiece] - for (i in pieceVariants.indices) { - if (patternsAreEquivalent(mirroredPattern, pieceVariants[i])) { - currentRotation = i - return true - } - } - - // If no matching rotation found, just use regular rotation as fallback - return rotate() - } else { - // Try wall kicks with the mirrored pattern - // Try moving right - if (!checkCollision(currentX + 1, currentY, mirroredPattern)) { - currentX++ - // Find equivalent rotation - val pieceVariants = pieces[currentPiece] - for (i in pieceVariants.indices) { - if (patternsAreEquivalent(mirroredPattern, pieceVariants[i])) { - currentRotation = i - return true - } - } - return rotate() - } - // Try moving left - if (!checkCollision(currentX - 1, currentY, mirroredPattern)) { - currentX-- - // Find equivalent rotation - val pieceVariants = pieces[currentPiece] - for (i in pieceVariants.indices) { - if (patternsAreEquivalent(mirroredPattern, pieceVariants[i])) { - currentRotation = i - return true - } - } - return rotate() - } - // Try moving up - if (!checkCollision(currentX, currentY - 1, mirroredPattern)) { - currentY-- - // Find equivalent rotation - val pieceVariants = pieces[currentPiece] - for (i in pieceVariants.indices) { - if (patternsAreEquivalent(mirroredPattern, pieceVariants[i])) { - currentRotation = i - return true - } - } - return rotate() - } - - // If all fails, don't change the actual piece, just visual effect - } - } - - // 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 flip the piece horizontally - 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 mirror the piece horizontally - if (rotation3DY % (maxRotation3D / 2) == 1) { - // Create a horizontally mirrored version of the current piece - val currentPattern = getCurrentPieceArray() - val rows = currentPattern.size - val cols = if (rows > 0) currentPattern[0].size else 0 - val mirroredPattern = Array(rows) { r -> Array(cols) { c -> currentPattern[r][cols - 1 - c] } } - - // Try to find an equivalent pattern in any piece type - // This allows for pieces to transform into different piece types when mirrored - for (pieceType in 0 until pieces.size) { - val pieceVariants = pieces[pieceType] - for (rotation in pieceVariants.indices) { - // Check if this variant matches our mirrored pattern - if (patternsAreEquivalent(mirroredPattern, pieceVariants[rotation])) { - // Transform into this piece type with this rotation - currentPiece = pieceType - currentRotation = rotation - currentColor = colors[currentPiece] - return true - } - } - } - - // If no exact match was found, find the most similar pattern - var bestPieceType = -1 - var bestRotation = -1 - var bestScore = -1 - - for (pieceType in 0 until pieces.size) { - val pieceVariants = pieces[pieceType] - for (rotation in pieceVariants.indices) { - val score = patternMatchScore(mirroredPattern, pieceVariants[rotation]) - if (score > bestScore) { - bestScore = score - bestPieceType = pieceType - bestRotation = rotation - } - } - } - - // If we found a reasonable match - if (bestScore > 0) { - // If this is a collision-free position, perform the transformation - if (!checkCollision(currentX, currentY, pieces[bestPieceType][bestRotation])) { - currentPiece = bestPieceType - currentRotation = bestRotation - currentColor = colors[currentPiece] - return true - } else { - // Try wall kicks with the new piece - // Try moving right - if (!checkCollision(currentX + 1, currentY, pieces[bestPieceType][bestRotation])) { - currentX++ - currentPiece = bestPieceType - currentRotation = bestRotation - currentColor = colors[currentPiece] - return true - } - // Try moving left - if (!checkCollision(currentX - 1, currentY, pieces[bestPieceType][bestRotation])) { - currentX-- - currentPiece = bestPieceType - currentRotation = bestRotation - currentColor = colors[currentPiece] - return true - } - // Try moving up - if (!checkCollision(currentX, currentY - 1, pieces[bestPieceType][bestRotation])) { - currentY-- - currentPiece = bestPieceType - currentRotation = bestRotation - currentColor = colors[currentPiece] - return true - } - } - } - - // If we couldn't find a good transformation, just do a regular rotation - // as a fallback to ensure some response to the user's action - 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 - } - - // 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 - } - } - } - } - } - - // Line clearing animation properties - private var lineClearEffect = false - private var clearedRows = mutableListOf() - private var lineClearProgress = 0f - private val maxLineClearDuration = 0.5f // In seconds - private var lineClearStartTime = 0L - - // Getter for line clear effect - fun isLineClearEffect(): Boolean = lineClearEffect - - // Get the cleared rows for animation - fun getClearedRows(): List = clearedRows - - // Get line clear animation progress (0-1) - fun getLineClearProgress(): Float = lineClearProgress - - private fun clearRows() { - clearedRows.clear() - - // First pass: identify full rows - 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) { - clearedRows.add(r) - } - } - - // If we have cleared rows, start the animation - if (clearedRows.isNotEmpty()) { - lineClearEffect = true - lineClearProgress = 0f - lineClearStartTime = System.currentTimeMillis() - - // The actual row clearing will be done when the animation completes - // This is handled in the updateLineClear method - - // The rows are still part of the board during animation but will be - // displayed with a special effect by the view - } else { - // No rows to clear, continue with normal gameplay - return - } - - // Mark the start of the animation - // The actual row clearance will happen after the animation - } - - // Update line clear animation - fun updateLineClear(): Boolean { - if (!lineClearEffect) return false - - // Calculate progress based on elapsed time - val elapsedTime = (System.currentTimeMillis() - lineClearStartTime) / 1000f - lineClearProgress = (elapsedTime / maxLineClearDuration).coerceIn(0f, 1f) - - // If animation is complete, apply the row clearing - if (lineClearProgress >= 1f) { - // Actually clear the rows and update score - completeLineClear() - - // Reset animation state - lineClearEffect = false - lineClearProgress = 0f - return true - } - - return false - } - - // Complete the line clearing after animation - private fun completeLineClear() { - val linesCleared = clearedRows.size - - // Process cleared rows in descending order to avoid index issues - val sortedRows = clearedRows.sortedDescending() - - for (row in sortedRows) { - // Move all rows above down - for (y in row 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 - } - } - - // 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 - - // Helper function to check if two patterns are equivalent (ignoring empty space) - private fun patternsAreEquivalent(pattern1: Array>, pattern2: Array>): Boolean { - // Quick check for size match - if (pattern1.size != pattern2.size) return false - if (pattern1.isEmpty() || pattern2.isEmpty()) return pattern1.isEmpty() && pattern2.isEmpty() - if (pattern1[0].size != pattern2[0].size) return false - - // Check if cells with 1s match in both patterns - for (r in pattern1.indices) { - for (c in pattern1[r].indices) { - if (pattern1[r][c] != pattern2[r][c]) { - return false - } - } - } - - return true - } - - // Helper function to score how well two patterns match (higher score = better match) - private fun patternMatchScore(pattern1: Array>, pattern2: Array>): Int { - // Quick check for size match - if (pattern1.size != pattern2.size) return 0 - if (pattern1.isEmpty() || pattern2.isEmpty()) return if (pattern1.isEmpty() && pattern2.isEmpty()) 1 else 0 - if (pattern1[0].size != pattern2[0].size) return 0 - - // Count matching cells - var matchCount = 0 - for (r in pattern1.indices) { - for (c in pattern1[r].indices) { - if (pattern1[r][c] == pattern2[r][c]) { - matchCount++ - } - } - } - - return matchCount - } -} \ 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 deleted file mode 100644 index afbb596..0000000 --- a/android-app/app/src/main/java/com/tetris3d/views/NextPieceView.kt +++ /dev/null @@ -1,128 +0,0 @@ -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 deleted file mode 100644 index f4ac309..0000000 --- a/android-app/app/src/main/java/com/tetris3d/views/TetrisGameView.kt +++ /dev/null @@ -1,861 +0,0 @@ -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 - private val showGlowEffects = true - - // Background gradient colors - private val bgColorStart = Color.parseColor("#06071B") // Darker space background - private val bgColorEnd = Color.parseColor("#0B1026") // Slightly lighter space background - private lateinit var bgGradient: LinearGradient - - // Glow and star effects - private val stars = ArrayList() - private val random = java.util.Random() - private val starCount = 50 - private val starColors = arrayOf( - Color.parseColor("#FFFFFF"), // White - Color.parseColor("#AAAAFF"), // Light blue - Color.parseColor("#FFAAAA"), // Light red - Color.parseColor("#AAFFAA") // Light green - ) - - // Gesture detection for swipe controls - private val gestureDetector = GestureDetector(context, TetrisGestureListener()) - - // Define minimum swipe velocity and distance - private val minSwipeVelocity = 30 // Lower for better responsiveness - private val minSwipeDistance = 15 // Lower for better responsiveness - - // Movement control - private val autoRepeatHandler = Handler(Looper.getMainLooper()) - private var isAutoRepeating = false - private var currentMovement: (() -> Unit)? = null - private val autoRepeatDelay = 100L // Faster for smoother continuous movement - private val initialAutoRepeatDelay = 150L // Faster initial delay - private val interpolator = DecelerateInterpolator(1.5f) - - // Touch tracking for continuous swipe - private var lastTouchX = 0f - private var lastTouchY = 0f - private var swipeThreshold = 20f // More sensitive - private var lastMoveTime = 0L - private val moveCooldown = 110L // Shorter cooldown for more responsive movement - private var tapThreshold = 10f // Slightly more forgiving tap detection - - // 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) - } - } - - // Game state flags - private var gameOver = false - private var paused = false - - companion object { - private const val REFRESH_INTERVAL = 16L // ~60fps - } - - fun setGame(game: TetrisGame) { - this.game = game - invalidate() - - // Start refresh timer - startRefreshTimer() - - // Update game state flags - gameOver = game.isGameOver - paused = !game.isRunning - } - - 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 - - // Initialize stars for background - initializeStars(w, h) - } - - private fun initializeStars(width: Int, height: Int) { - stars.clear() - for (i in 0 until starCount) { - stars.add(Star( - x = random.nextFloat() * width, - y = random.nextFloat() * height, - size = 1f + random.nextFloat() * 2f, - color = starColors[random.nextInt(starColors.size)], - blinkSpeed = 0.5f + random.nextFloat() * 2f - )) - } - } - - // Star class for background effect - private data class Star( - val x: Float, - val y: Float, - val size: Float, - val color: Int, - val blinkSpeed: Float, - var brightness: Float - ) { - companion object { - private val random = java.util.Random() - } - - constructor(x: Float, y: Float, size: Float, color: Int, blinkSpeed: Float) : this( - x = x, - y = y, - size = size, - color = color, - blinkSpeed = blinkSpeed, - brightness = random.nextFloat() - ) - } - - override fun onDraw(canvas: Canvas) { - super.onDraw(canvas) - - val game = this.game ?: return - - // Update game state flags - gameOver = game.isGameOver - paused = !game.isRunning - - // Draw space background with gradient - paint.shader = bgGradient - canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint) - paint.shader = null - - // Draw stars in background - drawStars(canvas) - - // Draw grid if enabled - if (showGrid) { - drawGrid(canvas) - } - - // Draw board border with enhanced glow effect - drawBoardBorder(canvas) - - // Draw the locked pieces on the board - drawBoard(canvas, game) - - // Draw line clear effect if active - if (game.isLineClearEffect()) { - drawLineClearEffect(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) - } - - // Update animations - if (game.isLineClearEffect()) { - // Update line clear animation - if (game.updateLineClear()) { - // If line clear animation completed, invalidate again - invalidate() - } else { - // Animation still in progress - invalidate() - } - } - - // Update star animation - updateStars() - } - - private fun updateStars() { - val currentTime = System.currentTimeMillis() / 1000f - for (star in stars) { - // Calculate pulsing brightness based on time and individual star speed - star.brightness = (kotlin.math.sin(currentTime * star.blinkSpeed) + 1f) / 2f - } - - // Force regular refresh to animate stars - if (!gameOver && !paused) { - postInvalidateDelayed(50) - } - } - - private fun drawStars(canvas: Canvas) { - paint.style = Paint.Style.FILL - - for (star in stars) { - // Set color with alpha based on brightness - paint.color = star.color - paint.alpha = (255 * star.brightness).toInt() - - // Draw star with glow effect - if (showGlowEffects) { - paint.setShadowLayer(star.size * 2, 0f, 0f, star.color) - } - - canvas.drawCircle(star.x, star.y, star.size * star.brightness, paint) - - // Reset shadow - if (showGlowEffects) { - paint.setShadowLayer(0f, 0f, 0f, Color.TRANSPARENT) - } - } - } - - 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 (enhanced cyan color) - paint.style = Paint.Style.STROKE - paint.strokeWidth = 4f - paint.color = Color.parseColor("#00ffff") - - if (showGlowEffects) { - // Stronger glow effect - paint.setShadowLayer(16f, 0f, 0f, Color.parseColor("#00ffff")) - } - - canvas.drawRect(borderRect, paint) - - // Inner glow - if (showGlowEffects) { - paint.strokeWidth = 2f - paint.color = Color.parseColor("#80ffff") - paint.setShadowLayer(8f, 0f, 0f, Color.parseColor("#80ffff")) - - val innerRect = RectF( - borderRect.left + 4f, - borderRect.top + 4f, - borderRect.right - 4f, - borderRect.bottom - 4f - ) - - canvas.drawRect(innerRect, paint) - } - - // Reset shadow - paint.setShadowLayer(0f, 0f, 0f, Color.TRANSPARENT) - } - - 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("#333344") // Slightly blue-tinted grid - 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, apply transformations, then translate back - canvas.translate(centerX, centerY) - - // Apply transformations based on rotation state - // First apply scaling to simulate flipping - val flipX = if (rotationX.toInt() % 2 == 1) -1f else 1f - val flipY = if (rotationY.toInt() % 2 == 1) -1f else 1f - - // Check if we're in the middle of an animation - if (game.isRotating()) { - // For animation, use perspective scaling and smooth transitions - val scaleX = cos(angleY.toFloat()).coerceAtLeast(0.5f) * flipY - val scaleY = cos(angleX.toFloat()).coerceAtLeast(0.5f) * flipX - canvas.scale(scaleX, scaleY) - } else { - // For static display, just flip directly - canvas.scale(flipY, flipX) - } - - // Translate back - canvas.translate(-centerX, -centerY) - - // Draw the piece with perspective or flip effect - for (r in piece.indices) { - for (c in piece[r].indices) { - if (piece[r][c] == 1) { - // Calculate offset for 3D effect during animation - val offsetX = if (game.isRotating()) sin(angleY.toFloat()) * blockSize * 0.3f else 0f - val offsetY = if (game.isRotating()) sin(angleX.toFloat()) * blockSize * 0.3f else 0f - - 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) - - // Parse the base color - val baseColor = Color.parseColor(colorStr) - - // Create a brighter version for the glow - val red = Color.red(baseColor) - val green = Color.green(baseColor) - val blue = Color.blue(baseColor) - val glowColor = Color.argb(255, - Math.min(255, red + 40), - Math.min(255, green + 40), - Math.min(255, blue + 40) - ) - - // Add glow effect - if (showGlowEffects) { - paint.style = Paint.Style.FILL - paint.color = baseColor - paint.setShadowLayer(blockSize / 4, 0f, 0f, glowColor) - } - - // Draw the block fill - paint.style = Paint.Style.FILL - paint.color = baseColor - canvas.drawRect(blockRect, paint) - - // Reset shadow - if (showGlowEffects) { - paint.setShadowLayer(0f, 0f, 0f, Color.TRANSPARENT) - } - - // Draw the highlight (top-left gradient) - paint.style = Paint.Style.FILL - val highlightPaint = Paint() - highlightPaint.shader = LinearGradient( - left, top, - right, bottom, - Color.argb(150, 255, 255, 255), // More pronounced highlight - 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) - - // Draw inner glow edge - if (showGlowEffects) { - paint.style = Paint.Style.STROKE - paint.strokeWidth = 1f - paint.color = glowColor - - val innerRect = RectF( - left + 2, - top + 2, - right - 2, - bottom - 2 - ) - canvas.drawRect(innerRect, paint) - } - } - - // Draw line clear effect - private fun drawLineClearEffect(canvas: Canvas, game: TetrisGame) { - val clearedRows = game.getClearedRows() - val progress = game.getLineClearProgress() - - // Different effect based on animation progress - for (row in clearedRows) { - for (col in 0 until TetrisGame.COLS) { - val left = boardLeft + col * blockSize - val top = boardTop + row * blockSize - val right = left + blockSize - val bottom = top + blockSize - - // Create a pulsing, brightening effect for cleared blocks - val alpha = (255 * (0.5f + 0.5f * Math.sin(progress * Math.PI * 3))).toInt() - val scale = 1.0f + 0.1f * progress - - // Calculate center for scaling - val centerX = left + blockSize / 2 - val centerY = top + blockSize / 2 - - // Save canvas state for transformation - canvas.save() - - // Position at center, scale, then move back - canvas.translate(centerX, centerY) - canvas.scale(scale, scale) - canvas.translate(-centerX, -centerY) - - // Get the color from the board - val color = game.getBoard()[row][col] - - if (color != TetrisGame.EMPTY) { - // Draw with glow effect - val baseColor = Color.parseColor(color) - - // Create a brighter glow as animation progresses - val red = Color.red(baseColor) - val green = Color.green(baseColor) - val blue = Color.blue(baseColor) - - // Get increasingly white as effect progresses - val whiteBlend = progress * 0.7f - val newRed = (red * (1 - whiteBlend) + 255 * whiteBlend).toInt().coerceIn(0, 255) - val newGreen = (green * (1 - whiteBlend) + 255 * whiteBlend).toInt().coerceIn(0, 255) - val newBlue = (blue * (1 - whiteBlend) + 255 * whiteBlend).toInt().coerceIn(0, 255) - - val effectColor = Color.argb(alpha, newRed, newGreen, newBlue) - - // Draw with glow - paint.style = Paint.Style.FILL - paint.color = effectColor - - if (showGlowEffects) { - // Increase glow radius with progress - val glowRadius = blockSize * (0.3f + 0.7f * progress) - paint.setShadowLayer(glowRadius, 0f, 0f, effectColor) - } - - // Draw the block - canvas.drawRect(left, top, right, bottom, paint) - - // Add horizontal line effect - paint.style = Paint.Style.STROKE - paint.strokeWidth = 4f * progress - canvas.drawLine(left, top + blockSize / 2, right, top + blockSize / 2, paint) - - // Reset shadow - if (showGlowEffects) { - paint.setShadowLayer(0f, 0f, 0f, Color.TRANSPARENT) - } - } - - // Restore canvas state - canvas.restore() - } - } - } - - // Handler for touch events - override fun onTouchEvent(event: MotionEvent): Boolean { - if (gameOver || paused) return false - - when (event.action) { - MotionEvent.ACTION_DOWN -> { - lastTouchX = event.x - lastTouchY = event.y - return true - } - MotionEvent.ACTION_MOVE -> { - val diffX = event.x - lastTouchX - val diffY = event.y - lastTouchY - val currentTime = System.currentTimeMillis() - - // Check if cooldown has elapsed since last move - if (currentTime - lastMoveTime < moveCooldown) { - return true - } - - // Check if drag distance exceeds threshold for movement - if (abs(diffX) > swipeThreshold && abs(diffX) > abs(diffY) * 1.2f) { - // Horizontal movement - requiring less pronounced horizontal movement for smoother control - if (diffX > 0) { - game?.moveRight() - } else { - game?.moveLeft() - } - // Update last position after processing the move - lastTouchX = event.x - lastMoveTime = currentTime - invalidate() - return true - } else if (abs(diffY) > swipeThreshold && abs(diffY) > abs(diffX) * 1.2f) { - // Vertical movement - requiring less pronounced vertical movement for smoother control - if (diffY > 0) { - game?.moveDown() - // Update last position after processing the move - lastTouchY = event.y - lastMoveTime = currentTime - invalidate() - return true - } - } - return true - } - MotionEvent.ACTION_UP -> { - val diffX = event.x - lastTouchX - val diffY = event.y - lastTouchY - val totalMovement = abs(diffX) + abs(diffY) - - // If this was a tap (very minimal movement) - if (totalMovement < tapThreshold) { - // Simple tap to rotate - game?.rotate() - invalidate() - return true - } - - // Check for deliberate swipe up (hard drop) - more forgiving upward movement - if (abs(diffY) > swipeThreshold * 1.2f && diffY < 0 && abs(diffY) > abs(diffX) * 1.5f) { - game?.hardDrop() - invalidate() - return true - } - - stopAutoRepeat() - return true - } - } - - return false - } - - 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 - } - - // We're handling taps directly in onTouchEvent - override fun onSingleTapConfirmed(e: MotionEvent): Boolean { - return false - } - - 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 - move right once - game?.moveRight() - } else { - // Swipe left - move left once - game?.moveLeft() - } - invalidate() - return true - } - } else { - // Vertical swipe - if (abs(velocityY) > minSwipeVelocity && abs(diffY) > minSwipeDistance) { - if (diffY > 0) { - // Swipe down - start soft drop - startAutoRepeat { game?.moveDown() } - } else { - // Swipe up - hard drop - game?.hardDrop() - } - invalidate() - return true - } - } - - return false - } - } - - // Create touch control buttons - fun createTouchControlButtons() { - // Create 3D rotation buttons - val context = context ?: return - - // First check if buttons are already added to prevent duplicates - val parent = parent as? android.view.ViewGroup ?: return - if (parent.findViewWithTag("rotate_buttons") != null) { - return - } - - // Create a container for rotation buttons - val rotateButtons = android.widget.LinearLayout(context) - rotateButtons.tag = "rotate_buttons" - rotateButtons.orientation = android.widget.LinearLayout.HORIZONTAL - rotateButtons.layoutParams = android.view.ViewGroup.LayoutParams( - android.view.ViewGroup.LayoutParams.MATCH_PARENT, - android.view.ViewGroup.LayoutParams.WRAP_CONTENT - ) - - // Add layout to position at bottom of screen - rotateButtons.gravity = android.view.Gravity.CENTER - val params = android.widget.FrameLayout.LayoutParams( - android.widget.FrameLayout.LayoutParams.MATCH_PARENT, - android.widget.FrameLayout.LayoutParams.WRAP_CONTENT - ) - params.gravity = android.view.Gravity.BOTTOM - params.setMargins(16, 16, 16, 32) // Add more bottom margin for visibility - rotateButtons.layoutParams = params - - // Create vertical flip button (X-axis rotation) - val verticalFlipButton = android.widget.Button(context) - verticalFlipButton.text = "Flip ↑↓" - verticalFlipButton.tag = "flip_vertical_button" - val buttonParams = android.widget.LinearLayout.LayoutParams( - 0, - android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, - 1.0f - ) - buttonParams.setMargins(12, 12, 12, 12) - verticalFlipButton.layoutParams = buttonParams - - // Style the button - verticalFlipButton.setBackgroundColor(android.graphics.Color.parseColor("#0088ff")) - verticalFlipButton.setTextColor(android.graphics.Color.WHITE) - verticalFlipButton.setPadding(8, 16, 8, 16) - - // Create horizontal flip button (Y-axis rotation) - val horizontalFlipButton = android.widget.Button(context) - horizontalFlipButton.text = "Flip ←→" - horizontalFlipButton.tag = "flip_horizontal_button" - horizontalFlipButton.layoutParams = buttonParams - - // Style the button - horizontalFlipButton.setBackgroundColor(android.graphics.Color.parseColor("#ff5500")) - horizontalFlipButton.setTextColor(android.graphics.Color.WHITE) - horizontalFlipButton.setPadding(8, 16, 8, 16) - - // Add buttons to container - rotateButtons.addView(verticalFlipButton) - rotateButtons.addView(horizontalFlipButton) - - // Add the button container to the parent view - val rootView = parent.rootView as? android.widget.FrameLayout - rootView?.addView(rotateButtons) - - // Add click listeners - verticalFlipButton.setOnClickListener { - if (!game?.isGameOver!! && game?.isRunning!!) { - game?.rotate3DX() - verticalFlipButton.alpha = 0.7f - android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ - verticalFlipButton.alpha = 1.0f - }, 150) - invalidate() - } - } - - horizontalFlipButton.setOnClickListener { - if (!game?.isGameOver!! && game?.isRunning!!) { - game?.rotate3DY() - horizontalFlipButton.alpha = 0.7f - android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ - horizontalFlipButton.alpha = 1.0f - }, 150) - invalidate() - } - } - - // Create and add instructions text view if needed - // Note: For Android implementation we'll show a toast instead of persistent instructions - val instructions = "Swipe to move, tap to rotate, swipe down for soft drop, swipe up for hard drop. Use buttons to flip pieces." - android.widget.Toast.makeText(context, instructions, android.widget.Toast.LENGTH_LONG).show() - } - - // Update the game state flags from the TetrisGame - fun updateGameState() { - game?.let { - gameOver = it.isGameOver - paused = !it.isRunning - invalidate() - } - } -} \ 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 deleted file mode 100644 index a4fb788..0000000 --- a/android-app/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ 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 deleted file mode 100644 index fcb9246..0000000 --- a/android-app/app/src/main/res/drawable/ic_launcher_foreground.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - \ 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 deleted file mode 100644 index c4989f1..0000000 --- a/android-app/app/src/main/res/drawable/simple_icon.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - \ 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 deleted file mode 100644 index 66fb2ba..0000000 --- a/android-app/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,183 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ 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 deleted file mode 100644 index ebf407c..0000000 --- a/android-app/app/src/main/res/layout/dialog_game_over.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - \ 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 deleted file mode 100644 index 6529074..0000000 --- a/android-app/app/src/main/res/layout/dialog_options.xml +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ 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 deleted file mode 100644 index d58b8f1..0000000 --- a/android-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ 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 deleted file mode 100644 index d58b8f1..0000000 --- a/android-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ 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 deleted file mode 100644 index 4f7aee8..0000000 --- a/android-app/app/src/main/res/mipmap-hdpi/ic_launcher.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - \ 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 deleted file mode 100644 index 4f7aee8..0000000 --- a/android-app/app/src/main/res/mipmap-hdpi/ic_launcher_round.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - \ 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 deleted file mode 100644 index 2299249..0000000 --- a/android-app/app/src/main/res/mipmap-mdpi/ic_launcher.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - \ 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 deleted file mode 100644 index 2299249..0000000 --- a/android-app/app/src/main/res/mipmap-mdpi/ic_launcher_round.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - \ 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 deleted file mode 100644 index dfcd653..0000000 --- a/android-app/app/src/main/res/mipmap-xhdpi/ic_launcher.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - \ 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 deleted file mode 100644 index dfcd653..0000000 --- a/android-app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - \ 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 deleted file mode 100644 index 3e5bb1d..0000000 --- a/android-app/app/src/main/res/mipmap-xxhdpi/ic_launcher.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - \ 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 deleted file mode 100644 index 3e5bb1d..0000000 --- a/android-app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - \ 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 deleted file mode 100644 index c4a2986..0000000 --- a/android-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - \ 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 deleted file mode 100644 index c4a2986..0000000 --- a/android-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - \ 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 deleted file mode 100644 index c2d02d1..0000000 --- a/android-app/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - #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 deleted file mode 100644 index 833398e..0000000 --- a/android-app/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,19 +0,0 @@ - - 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 deleted file mode 100644 index 05d2ed1..0000000 --- a/android-app/app/src/main/res/values/themes.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - \ No newline at end of file diff --git a/android-app/build.gradle b/android-app/build.gradle deleted file mode 100644 index 3f86daa..0000000 --- a/android-app/build.gradle +++ /dev/null @@ -1,22 +0,0 @@ -// 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 deleted file mode 100644 index cb3e8f3..0000000 --- a/android-app/build/reports/problems/problems-report.html +++ /dev/null @@ -1,663 +0,0 @@ - - - - - - - - - - - - - Gradle Configuration Cache - - - - - - - Loading... - - - - - - - diff --git a/android-app/gradle.properties b/android-app/gradle.properties deleted file mode 100644 index a84c3d0..0000000 --- a/android-app/gradle.properties +++ /dev/null @@ -1,19 +0,0 @@ -# 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 deleted file mode 100644 index a4b76b9..0000000 Binary files a/android-app/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/android-app/gradle/wrapper/gradle-wrapper.properties b/android-app/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index e2847c8..0000000 --- a/android-app/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100755 index f5feea6..0000000 --- a/android-app/gradlew +++ /dev/null @@ -1,252 +0,0 @@ -#!/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 deleted file mode 100644 index 9b42019..0000000 --- a/android-app/gradlew.bat +++ /dev/null @@ -1,94 +0,0 @@ -@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 deleted file mode 100644 index 5ef8021..0000000 --- a/android-app/local.properties +++ /dev/null @@ -1,8 +0,0 @@ -## 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 deleted file mode 100644 index 9781148..0000000 --- a/android-app/settings.gradle +++ /dev/null @@ -1,2 +0,0 @@ -rootProject.name = "Tetris3D" -include ':app' \ No newline at end of file diff --git a/index.html b/index.html index a3ab14c..ea7f4d5 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,9 @@ - + + + 3D Tetris @@ -36,11 +38,11 @@ A / ← Move Left D / → Move Right S / ↓ Move Down - ↑ / Space Hard Drop Q Rotate Left E Rotate Right W Horizontal 3D X Vertical 3D + Space Hard Drop P Pause Game H Toggle Shadow @@ -51,11 +53,11 @@ D-Pad ←/→ Move Left/Right D-Pad ↓ Move Down - D-Pad ↑ Hard Drop A Rotate Left B Rotate Right Y Horizontal 3D X Vertical 3D + RT Hard Drop Start Pause Game @@ -78,42 +80,35 @@ - Options + Game Options - 3D Effects - - - - - Enable 3D block rendering for a more immersive experience + Enable 3D Effects + - Spin Animations - - - - - Show 3D rotation animations when rotating pieces + Enable Spin Animations + Animation Speed - Adjust the speed of 3D animations + + + + Force Mobile Controls + - Starting Level - - Set the level you want to start at (1-10) + Performance Mode + + Reduces visual effects for better performance on mobile devices - - Close - Apply - + Close diff --git a/script.js b/script.js index d44a24a..2c53eb2 100644 --- a/script.js +++ b/script.js @@ -70,7 +70,6 @@ let enableSpinAnimations = true; let animationSpeed = 0.05; let forceMobileControls = false; let reduceEffectsOnMobile = true; // New option to reduce effects on mobile -let startingLevel = 1; // New option for starting level // Controller variables let gamepadConnected = false; @@ -79,28 +78,16 @@ let controllerMapping = { left: [14, 'dpadLeft'], // D-pad left or left stick left right: [15, 'dpadRight'], // D-pad right or left stick right down: [13, 'dpadDown'], // D-pad down or left stick down - up: [12, 'dpadUp'], // D-pad up (added for hard drop) rotateLeft: [0, 'buttonA'], // A button rotateRight: [1, 'buttonB'], // B button mirrorH: [3, 'buttonY'], // Y button - horizontal mirror mirrorV: [2, 'buttonX'], // X button - vertical mirror - hardDrop: [12, 'dpadUp'], // Changed from RT to D-pad up + hardDrop: [7, 'rightTrigger'], // RT button pause: [9, 'start'] // Start button }; let lastControllerState = {}; -let controllerPollingRate = 16; // Increased polling rate (from 100ms to 16ms for ~60fps) +let controllerPollingRate = 100; // ms let controllerInterval; -let buttonHoldDelays = { - left: 150, // Initial delay before repeating - right: 150, // Initial delay before repeating - down: 50 // Fast repeat for down -}; -let buttonRepeatRates = { - left: 100, // Repeat rate after initial delay - right: 100, // Repeat rate after initial delay - down: 50 // Fast repeat rate for down -}; -let buttonHoldTimers = {}; // Fireworks array let fireworks = []; @@ -589,67 +576,78 @@ class Piece { // Rotate with 3D animation rotate(direction) { - // Skip if already in an animation - if (this.rotationTransition || this.showCompletionEffect) { - return; - } + // Direction can be 'right' or 'left' + if (gameOver || paused) return; - // Reset animation state - this.resetAnimationState(); - - // Square pieces (O) don't need to rotate + // Square piece (O) doesn't rotate if (this.tetrominoType === 'O') { return; } - // If animations are disabled, do simple rotation - if (!enableSpinAnimations || !enable3DEffects) { - // Use standard rotation without animation - let nextPattern = (direction === 'right') ? - (this.tetrominoN + 1) % this.tetromino.length : - (this.tetrominoN + this.tetromino.length - 1) % this.tetromino.length; - - // Try to rotate and apply kicks if needed - const kickResult = this.tryRotateWithKicks(nextPattern); - - if (kickResult.success) { - // Apply rotation - this.x += kickResult.kick; - this.tetrominoN = nextPattern; - this.activeTetromino = this.tetromino[this.tetrominoN]; - this.shadowTetromino = this.activeTetromino; - - // Update shadow - this.calculateShadowY(); - - // Play sound - playPieceSound('rotate'); - } - - // Draw the piece - this.draw(); - return; - } - - // Find the next pattern based on rotation direction - let nextPattern = (direction === 'right') ? - (this.tetrominoN + 1) % this.tetromino.length : - (this.tetrominoN + this.tetromino.length - 1) % this.tetromino.length; - - // Try to rotate with kicks - const kickResult = this.tryRotateWithKicks(nextPattern); - - if (!kickResult.success) { - return; // Rotation failed - } - - // We found a valid kick - save for animation completion - const validKick = kickResult.kick; - // Clear previous position this.undraw(); clearPreviousPiecePosition(); + // For I tetromino, rotation pattern is different (cycles through 0-1) + // For other tetrominoes, it cycles through 0-1-2-3 + let nextPattern; + + if (direction === 'right') { + // Rotate clockwise + if (this.tetrominoType === 'I') { + nextPattern = (this.tetrominoN + 1) % 2; + } else { + nextPattern = (this.tetrominoN + 1) % 4; + } + } else if (direction === 'left') { + // Rotate counter-clockwise + if (this.tetrominoType === 'I') { + nextPattern = (this.tetrominoN - 1 + 2) % 2; + } else { + nextPattern = (this.tetrominoN - 1 + 4) % 4; + } + } + + // Wall kick testing for rotation + // Try the standard position first, then the kickTable positions + const kicks = this.getKicks(this.tetrominoN, nextPattern); + + // Try each kick position until we find one that works + let validKick = null; + + for (let i = 0; i < kicks.length; i++) { + const [kickX, kickY] = kicks[i]; + + if (!this.collision(kickX, kickY, this.tetromino[nextPattern])) { + validKick = kickX; + // If a successful position is found, break the loop + break; + } + } + + // If no valid kick position found, keep the original position + if (validKick === null) { + this.draw(); + return; + } + + // If animations disabled, apply rotation immediately + if (!enableSpinAnimations) { + this.x += validKick; + this.tetrominoN = nextPattern; + this.activeTetromino = this.tetromino[this.tetrominoN]; + this.shadowTetromino = this.activeTetromino; + + // Play rotation sound + playPieceSound('rotate'); + + // Update shadow position + this.calculateShadowY(); + + this.draw(); + return; + } + // Store for animation completion this.targetPattern = nextPattern; this.targetKick = validKick; @@ -677,14 +675,20 @@ class Piece { return; } - // First, completely reset all animation state variables - // to prevent any lingering effects from rotations - this.resetAnimationState(); + // Cancel any ongoing rotations or animations + this.rotationTransition = false; + this.showCompletionEffect = false; + this.rotationAngleX = 0; + this.rotationAngleY = 0; + this.rotationAngleZ = 0; - // Save current state for proper undraw - const currentTetromino = this.activeTetromino; - const currentX = this.x; - const currentY = this.y; + // Clear rotation state completely + this.rotationDirection = null; + this.targetTetromino = null; + this.targetPattern = undefined; + this.targetKick = undefined; + this.originalTetromino = null; + this.rotationProgress = 0; // Clear previous position this.undraw(); @@ -695,40 +699,22 @@ class Piece { this.y++; } - // Lock the piece at its final position + // Lock the piece this.lock(); - // Get the next piece + // Replace with getNextPiece function call instead of direct assignment getNextPiece(); // Calculate shadow for new piece p.calculateShadowY(); - // Draw the new piece and board - drawBoard(); + // Draw the new piece p.draw(); // Play hard drop sound playPieceSound('hardDrop'); } - // Reset all animation state - resetAnimationState() { - this.rotationTransition = false; - this.showCompletionEffect = false; - this.rotationAngleX = 0; - this.rotationAngleY = 0; - this.rotationAngleZ = 0; - this.rotationDirection = null; - this.rotationProgress = 0; - this.rotationEasing = false; - this.completionEffectProgress = 0; - this.targetTetromino = null; - this.targetPattern = undefined; - this.targetKick = undefined; - this.originalTetromino = null; - } - // 3D horizontal rotation effect (around Y axis) rotate3DY() { // Square pieces (O) shouldn't rotate @@ -736,14 +722,6 @@ class Piece { return; } - // Skip if already in an animation - if (this.rotationTransition || this.showCompletionEffect) { - return; - } - - // Reset animation state - this.resetAnimationState(); - // Clear previous position this.undraw(); clearPreviousPiecePosition(); @@ -805,14 +783,6 @@ class Piece { return; } - // Skip if already in an animation - if (this.rotationTransition || this.showCompletionEffect) { - return; - } - - // Reset animation state - this.resetAnimationState(); - // Clear previous position this.undraw(); clearPreviousPiecePosition(); @@ -1098,12 +1068,31 @@ class Piece { this.shadowTetromino = this.targetTetromino; } - // Completely reset rotation state - this.resetAnimationState(); + // Reset rotation state + this.rotationTransition = false; + this.rotationProgress = 0; + + // Clear target references to avoid memory leaks and state issues + this.targetTetromino = null; + this.targetPattern = undefined; + this.targetKick = undefined; + this.originalTetromino = null; + + // Reset rotation angles + this.rotationAngleX = 0; + this.rotationAngleY = 0; + this.rotationAngleZ = 0; // Recalculate shadow position this.calculateShadowY(); + // Skip completion effect on mobile for performance + if (!mobilePerformanceMode && enable3DEffects) { + // Start completion effect + this.showCompletionEffect = true; + this.completionEffectProgress = 0; + } + // Draw in new position this.draw(); return; @@ -1134,12 +1123,18 @@ class Piece { this.completionEffectProgress += completionStep; if (this.completionEffectProgress >= 1) { - // Completely reset animation state - this.resetAnimationState(); + this.showCompletionEffect = false; + this.completionEffectProgress = 0; - // Redraw with final state - this.draw(); - return; + // Reset rotation angles + this.rotationAngleX = 0; + this.rotationAngleY = 0; + this.rotationAngleZ = 0; + + // Clear all rotation references + this.rotationDirection = null; + this.originalTetromino = null; + this.targetTetromino = null; } } @@ -1321,24 +1316,6 @@ class Piece { return kickTable[key] || [[0, 0]]; } } - - // Try rotation with wall kicks - tryRotateWithKicks(nextPattern) { - // Get kicks for this rotation - const kicks = this.getKicks(this.tetrominoN, nextPattern); - - // Try each kick position until we find one that works - for (let i = 0; i < kicks.length; i++) { - const [kickX, kickY] = kicks[i]; - - if (!this.collision(kickX, kickY, this.tetromino[nextPattern])) { - return { success: true, kick: kickX }; - } - } - - // No valid kick position found - return { success: false, kick: 0 }; - } } @@ -1788,58 +1765,16 @@ function dropPiece() { // Show game over modal function showGameOver() { - // Stop game interval clearInterval(gameInterval); - - // Clear all controller button hold timers - clearAllButtonHoldTimers(); - - // Update final score finalScoreElement.textContent = score; - - // Show modal gameOverModal.classList.add('active'); } -// Clear all button hold timers -function clearAllButtonHoldTimers() { - ['left', 'right', 'down'].forEach(action => { - if (buttonHoldTimers[action]) { - clearInterval(buttonHoldTimers[action]); - buttonHoldTimers[action] = null; - } - }); -} - -// Toggle pause -function togglePause() { - if (gameOver) return; - - paused = !paused; - - if (paused) { - clearInterval(gameInterval); - // Clear all controller button hold timers - clearAllButtonHoldTimers(); - pauseBtn.textContent = 'Resume'; - // Show pause message - showMessage('PAUSED', 'Press P to Resume'); - } else { - let speed = Math.max(100, 1000 - (level * 100)); - gameInterval = setInterval(dropPiece, speed); - pauseBtn.textContent = 'Pause'; - // Hide pause message - hideMessage(); - } -} - // Reset the game function resetGame() { // Reset game variables score = 0; - console.log("Starting level is set to:", startingLevel); - level = startingLevel; - console.log("Level is now set to:", level); + level = 1; lines = 0; gameOver = false; paused = false; @@ -1867,18 +1802,26 @@ function resetGame() { // Start the game interval dropStart = Date.now(); clearInterval(gameInterval); - // Set interval based on starting level - let speed = Math.max(100, 1000 - (level * 100)); - console.log("Game speed set to:", speed, "ms"); - gameInterval = setInterval(dropPiece, speed); + gameInterval = setInterval(dropPiece, 1000); +} + +// Toggle pause +function togglePause() { + paused = !paused; + + if (paused) { + clearInterval(gameInterval); + pauseBtn.textContent = "Resume"; + } else { + gameInterval = setInterval(dropPiece, Math.max(100, 1000 - (level * 100))); + pauseBtn.textContent = "Pause"; + } } // Toggle options modal function toggleOptionsModal() { if (optionsModal.classList.contains('active')) { optionsModal.classList.remove('active'); - // Apply options when closing the modal - applyOptions(); } else { optionsModal.classList.add('active'); } @@ -1890,7 +1833,6 @@ function applyOptions() { enableSpinAnimations = toggleSpinAnimations.checked; animationSpeed = parseFloat(animationSpeedSlider.value); forceMobileControls = toggleMobileControls.checked; - startingLevel = parseInt(document.getElementById('starting-level').value); // Apply mobile controls if checked or if on mobile device if (forceMobileControls) { @@ -1925,8 +1867,7 @@ function saveOptions() { enableSpinAnimations, animationSpeed, forceMobileControls, - reduceEffectsOnMobile, - startingLevel + reduceEffectsOnMobile })); } @@ -1941,7 +1882,6 @@ function loadOptions() { animationSpeed = options.animationSpeed !== undefined ? options.animationSpeed : 0.05; forceMobileControls = options.forceMobileControls !== undefined ? options.forceMobileControls : false; reduceEffectsOnMobile = options.reduceEffectsOnMobile !== undefined ? options.reduceEffectsOnMobile : true; - startingLevel = options.startingLevel !== undefined ? options.startingLevel : 1; // Update UI controls toggle3DEffects.checked = enable3DEffects; @@ -1955,10 +1895,6 @@ function loadOptions() { if (document.getElementById('toggle-mobile-performance')) { document.getElementById('toggle-mobile-performance').checked = reduceEffectsOnMobile; } - - if (document.getElementById('starting-level')) { - document.getElementById('starting-level').value = startingLevel; - } } } @@ -1970,10 +1906,6 @@ function init() { // Load saved options loadOptions(); - // Set initial level - level = startingLevel; - levelElement.textContent = level; - // Draw the board drawBoard(); @@ -1986,9 +1918,7 @@ function init() { // Set up game interval dropStart = Date.now(); - // Use level-appropriate speed - let speed = Math.max(100, 1000 - (level * 100)); - gameInterval = setInterval(dropPiece, speed); + gameInterval = setInterval(dropPiece, 1000); // Listen for keyboard events document.addEventListener('keydown', control); @@ -2007,14 +1937,6 @@ function init() { optionsBtn.addEventListener('click', toggleOptionsModal); optionsCloseBtn.addEventListener('click', toggleOptionsModal); - // Add event listener for the apply button - if (document.getElementById('options-apply-btn')) { - document.getElementById('options-apply-btn').addEventListener('click', function() { - applyOptions(); - console.log("Options applied, starting level is now:", startingLevel); - }); - } - // Options event listeners toggle3DEffects.addEventListener('change', function() { applyOptions(); @@ -2028,12 +1950,6 @@ function init() { applyOptions(); }); - if (document.getElementById('starting-level')) { - document.getElementById('starting-level').addEventListener('change', function() { - applyOptions(); - }); - } - if (document.getElementById('toggle-mobile-performance')) { document.getElementById('toggle-mobile-performance').addEventListener('change', function() { mobilePerformanceMode = this.checked; @@ -2072,17 +1988,68 @@ function handleResize() { const gameWrapper = document.querySelector('.game-wrapper'); const scoreContainer = document.querySelector('.score-container'); - // 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(); + 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; + } } + + // 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'); } } @@ -2107,7 +2074,7 @@ function initControllerSupport() { gamepadConnected = true; controllers[e.gamepad.index] = e.gamepad; - // Start polling for controller input at faster rate + // Start polling for controller input if not already if (!controllerInterval) { controllerInterval = setInterval(pollControllers, controllerPollingRate); } @@ -2125,18 +2092,14 @@ function initControllerSupport() { gamepadConnected = false; clearInterval(controllerInterval); controllerInterval = null; - - // Clear any ongoing button holds - clearAllButtonHoldTimers(); } // Show controller disconnected message showControllerMessage('Controller disconnected'); }); - // Initial scan and setup + // Initial scan if (Object.keys(controllers).length > 0) { - gamepadConnected = true; controllerInterval = setInterval(pollControllers, controllerPollingRate); } } @@ -2169,13 +2132,10 @@ function pollControllers() { if (!lastControllerState[controller.index]) { lastControllerState[controller.index] = { buttons: Array(controller.buttons.length).fill(false), - axes: Array(controller.axes.length).fill(0), - holdStartTimes: {} + axes: Array(controller.axes.length).fill(0) }; } - const now = Date.now(); - // Check buttons for (let action in controllerMapping) { const buttonIndex = controllerMapping[action][0]; @@ -2184,42 +2144,12 @@ function pollControllers() { if (buttonIndex >= 0 && buttonIndex < controller.buttons.length) { const button = controller.buttons[buttonIndex]; const pressed = button.pressed || button.value > 0.5; - const wasPressed = lastControllerState[controller.index].buttons[buttonIndex]; - // New button press (wasn't pressed last time) - if (pressed && !wasPressed) { - // Start tracking hold time for repeatable actions - if (['left', 'right', 'down'].includes(action)) { - lastControllerState[controller.index].holdStartTimes[action] = now; - // Clear any existing timers - if (buttonHoldTimers[action]) { - clearInterval(buttonHoldTimers[action]); - } - - // Schedule repeating actions - buttonHoldTimers[action] = setInterval(() => { - if (gamepadConnected && !gameOver && !paused) { - handleControllerAction(action); - } else { - // Stop repeating if game state changes - clearInterval(buttonHoldTimers[action]); - buttonHoldTimers[action] = null; - } - }, buttonRepeatRates[action]); - } - - // Immediate action on first press + // Check if this is a new press (wasn't pressed last time) + if (pressed && !lastControllerState[controller.index].buttons[buttonIndex]) { + // Handle controller action handleControllerAction(action); } - // Button released - else if (!pressed && wasPressed) { - // Stop repeating on release - if (['left', 'right', 'down'].includes(action) && buttonHoldTimers[action]) { - clearInterval(buttonHoldTimers[action]); - buttonHoldTimers[action] = null; - delete lastControllerState[controller.index].holdStartTimes[action]; - } - } // Update state lastControllerState[controller.index].buttons[buttonIndex] = pressed; @@ -2227,94 +2157,17 @@ function pollControllers() { } // Handle analog stick for movement - const deadzone = 0.5; - // Left stick horizontal - if (controller.axes[0] < -deadzone) { - if (lastControllerState[controller.index].axes[0] >= -deadzone) { - // Initial movement - handleControllerAction('left'); - - // Setup holding behavior - lastControllerState[controller.index].holdStartTimes['left'] = now; - if (buttonHoldTimers['left']) clearInterval(buttonHoldTimers['left']); - - buttonHoldTimers['left'] = setInterval(() => { - if (gamepadConnected && !gameOver && !paused) { - handleControllerAction('left'); - } else { - clearInterval(buttonHoldTimers['left']); - buttonHoldTimers['left'] = null; - } - }, buttonRepeatRates['left']); - } + if (controller.axes[0] < -0.5 && lastControllerState[controller.index].axes[0] >= -0.5) { + handleControllerAction('left'); } - else if (controller.axes[0] > deadzone) { - if (lastControllerState[controller.index].axes[0] <= deadzone) { - // Initial movement - handleControllerAction('right'); - - // Setup holding behavior - lastControllerState[controller.index].holdStartTimes['right'] = now; - if (buttonHoldTimers['right']) clearInterval(buttonHoldTimers['right']); - - buttonHoldTimers['right'] = setInterval(() => { - if (gamepadConnected && !gameOver && !paused) { - handleControllerAction('right'); - } else { - clearInterval(buttonHoldTimers['right']); - buttonHoldTimers['right'] = null; - } - }, buttonRepeatRates['right']); - } - } - else { - // Stick returned to center, clear horizontal timers - if (Math.abs(lastControllerState[controller.index].axes[0]) > deadzone) { - ['left', 'right'].forEach(action => { - if (buttonHoldTimers[action]) { - clearInterval(buttonHoldTimers[action]); - buttonHoldTimers[action] = null; - } - }); - } + else if (controller.axes[0] > 0.5 && lastControllerState[controller.index].axes[0] <= 0.5) { + handleControllerAction('right'); } - // Left stick vertical - down only - if (controller.axes[1] > deadzone) { - if (lastControllerState[controller.index].axes[1] <= deadzone) { - // Initial movement - handleControllerAction('down'); - - // Setup holding behavior - lastControllerState[controller.index].holdStartTimes['down'] = now; - if (buttonHoldTimers['down']) clearInterval(buttonHoldTimers['down']); - - buttonHoldTimers['down'] = setInterval(() => { - if (gamepadConnected && !gameOver && !paused) { - handleControllerAction('down'); - } else { - clearInterval(buttonHoldTimers['down']); - buttonHoldTimers['down'] = null; - } - }, buttonRepeatRates['down']); // Faster repeat for down - } - } - // Up direction for hard drop - else if (controller.axes[1] < -deadzone) { - if (lastControllerState[controller.index].axes[1] >= -deadzone) { - // One-time action for hard drop (no need for repeat) - handleControllerAction('up'); - } - } - else { - // Stick returned to center, clear down timer - if (Math.abs(lastControllerState[controller.index].axes[1]) > deadzone) { - if (buttonHoldTimers['down']) { - clearInterval(buttonHoldTimers['down']); - buttonHoldTimers['down'] = null; - } - } + // Left stick vertical + if (controller.axes[1] > 0.5 && lastControllerState[controller.index].axes[1] <= 0.5) { + handleControllerAction('down'); } // Update axes state @@ -2325,14 +2178,6 @@ function pollControllers() { function handleControllerAction(action) { if (paused && action !== 'pause') return; - // Skip all actions if currently in an animation - if (p.rotationTransition || p.showCompletionEffect) { - if (action === 'pause') { - togglePause(); - } - return; - } - switch(action) { case 'left': p.moveLeft(); @@ -2343,10 +2188,6 @@ function handleControllerAction(action) { case 'down': p.moveDown(); break; - case 'up': - case 'hardDrop': - p.hardDrop(); - break; case 'rotateLeft': p.rotate('left'); break; @@ -2359,6 +2200,12 @@ function handleControllerAction(action) { case 'mirrorV': p.mirrorVertical(); break; + case 'hardDrop': + // Only perform hard drop if not in the middle of an animation + if (!p.rotationTransition && !p.showCompletionEffect) { + p.hardDrop(); + } + break; case 'pause': togglePause(); break; @@ -2403,21 +2250,12 @@ function control(event) { if (gameOver) return; if (paused && event.keyCode !== 80) return; // Allow only P key if paused - // Skip all actions if currently in an animation - if (p.rotationTransition || p.showCompletionEffect) { - if (event.keyCode === 80) { // P key for pause still works - togglePause(); - } - return; - } - switch(event.keyCode) { case 37: // Left arrow case 65: // A key p.moveLeft(); break; - case 38: // Up arrow - p.hardDrop(); + case 38: // Up arrow - no function break; case 39: // Right arrow case 68: // D key @@ -2876,98 +2714,4 @@ window.onload = function() { // Force resize to ensure proper mobile layout window.dispatchEvent(new Event('resize')); } -}; - -// Check for completed rows and clear them -function checkRows() { - let linesCleared = 0; - let clearedRows = []; - - for (let r = 0; r < ROWS; r++) { - let isRowFull = true; - - // Check if the row is full - for (let c = 0; c < COLS; c++) { - if (board[r][c] === EMPTY) { - isRowFull = false; - break; - } - } - - // If the row is full, clear it - if (isRowFull) { - clearedRows.push(r); - linesCleared++; - - // Shift rows down - for (let y = r; y > 0; y--) { - for (let c = 0; c < COLS; c++) { - board[y][c] = board[y-1][c]; - } - } - - // Clear the top row - for (let c = 0; c < COLS; c++) { - board[0][c] = EMPTY; - } - } - } - - // Create fireworks for each cleared row - if (clearedRows.length > 0) { - // Reduce number of fireworks on mobile for better performance - const fireworksPerRow = mobilePerformanceMode ? 1 : 3; - - for (let i = 0; i < clearedRows.length; i++) { - // Create multiple fireworks along the row - for (let j = 0; j < fireworksPerRow; j++) { - const x = (Math.random() * COLS * BLOCK_SIZE) + BLOCK_SIZE/2; - const y = (clearedRows[i] * BLOCK_SIZE) + BLOCK_SIZE/2; - fireworks.push(new Firework(x, y)); - } - } - - // Play sound effect for line clear - playLineClearSound(clearedRows.length); - } - - return linesCleared; -} - -// Update score based on lines cleared -function updateScore() { - // Get number of lines cleared - let linesCleared = checkRows(); - - // Update score - if (linesCleared > 0) { - // Points increase for multiple lines cleared at once - const linePoints = [0, 100, 300, 500, 800]; // 0, 1, 2, 3, 4 lines - score += linePoints[linesCleared] * level; - lines += linesCleared; - - // Level up every 10 lines - if (Math.floor(lines / 10) > level - 1) { - level = Math.floor(lines / 10) + 1; - // Speed up the game as level increases - clearInterval(gameInterval); - gameInterval = setInterval(dropPiece, Math.max(100, 1000 - (level * 100))); - } - - // Update UI - scoreElement.textContent = score; - levelElement.textContent = level; - linesElement.textContent = lines; - } -} - -// Simple function to play audio -function playSound(audioElement) { - // Reset the audio to the beginning - audioElement.currentTime = 0; - // Play the sound - audioElement.play().catch(e => { - // Suppress errors from autoplay restrictions - console.log("Sound play failed:", e); - }); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/style.css b/style.css index bb84ba2..e52a7de 100644 --- a/style.css +++ b/style.css @@ -385,9 +385,8 @@ canvas#tetris { /* Options menu styles */ .option-row { display: flex; - justify-content: space-between; align-items: center; - margin-bottom: 15px; + margin: 15px 0; position: relative; } @@ -404,11 +403,16 @@ 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; @@ -499,7 +503,14 @@ input[type=range]::-moz-range-thumb { box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); } -/* 3D Rotation buttons - keep these for desktop */ +/* Mobile-specific styles */ +.mobile-mode { + overflow: hidden; + overscroll-behavior: none; + touch-action: none; +} + +/* 3D Rotation buttons */ .rotate-buttons { position: fixed; bottom: 20px; @@ -529,21 +540,258 @@ input[type=range]::-moz-range-thumb { background: linear-gradient(45deg, #0062cc, #00b3ff); } -/* Performance toggle - Since we're removing mobile support */ -/* Removing mobile-specific media queries and styles */ +/* 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; +} -/* Removing touch-instructions class and related styles */ .instructions-btn { margin-top: 15px; background: linear-gradient(45deg, #ff9900, #ff5500); -} - -.button-row { - display: flex; - justify-content: space-between; - margin-top: 20px; -} - -.button-row .game-btn { - margin: 0 5px; } \ No newline at end of file
A / ← Move Left
D / → Move Right
S / ↓ Move Down
↑ / Space Hard Drop
Q Rotate Left
E Rotate Right
W Horizontal 3D
X Vertical 3D
Space Hard Drop
P Pause Game
H Toggle Shadow
D-Pad ←/→ Move Left/Right
D-Pad ↓ Move Down
D-Pad ↑ Hard Drop
A Rotate Left
B Rotate Right
Y Horizontal 3D
RT Hard Drop
Start Pause Game