├── .idea
├── .name
├── .gitignore
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
├── compiler.xml
├── kotlinc.xml
├── vcs.xml
├── migrations.xml
├── deploymentTargetSelector.xml
├── misc.xml
├── gradle.xml
├── runConfigurations.xml
└── inspectionProfiles
│ └── Project_Default.xml
├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── themes.xml
│ │ │ │ └── colors.xml
│ │ │ ├── raw
│ │ │ │ ├── sound_0.wav
│ │ │ │ ├── sound_1.wav
│ │ │ │ ├── sound_2.wav
│ │ │ │ ├── sound_3.wav
│ │ │ │ ├── sound_4.wav
│ │ │ │ ├── sound_5.wav
│ │ │ │ ├── sound_6.wav
│ │ │ │ ├── sound_7.wav
│ │ │ │ ├── sound_8.wav
│ │ │ │ ├── sound_9.wav
│ │ │ │ ├── sound_a.wav
│ │ │ │ ├── sound_b.wav
│ │ │ │ ├── sound_c.wav
│ │ │ │ ├── sound_d.wav
│ │ │ │ ├── sound_e.wav
│ │ │ │ ├── sound_f.wav
│ │ │ │ ├── sound_g.wav
│ │ │ │ ├── sound_h.wav
│ │ │ │ ├── sound_i.wav
│ │ │ │ ├── sound_j.wav
│ │ │ │ ├── sound_k.wav
│ │ │ │ ├── sound_l.wav
│ │ │ │ ├── sound_m.wav
│ │ │ │ ├── sound_n.wav
│ │ │ │ ├── sound_o.wav
│ │ │ │ ├── sound_p.wav
│ │ │ │ ├── sound_q.wav
│ │ │ │ ├── sound_r.wav
│ │ │ │ ├── sound_s.wav
│ │ │ │ ├── sound_t.wav
│ │ │ │ ├── sound_u.wav
│ │ │ │ ├── sound_v.wav
│ │ │ │ ├── sound_w.wav
│ │ │ │ ├── sound_x.wav
│ │ │ │ ├── sound_y.wav
│ │ │ │ └── sound_z.wav
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── layout
│ │ │ │ └── activity_main.xml
│ │ │ ├── xml
│ │ │ │ ├── backup_rules.xml
│ │ │ │ └── data_extraction_rules.xml
│ │ │ └── drawable
│ │ │ │ ├── ic_launcher_foreground.xml
│ │ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── snake
│ │ │ │ ├── Direction.kt
│ │ │ │ ├── ui
│ │ │ │ └── theme
│ │ │ │ │ ├── Color.kt
│ │ │ │ │ ├── Type.kt
│ │ │ │ │ └── Theme.kt
│ │ │ │ ├── Food.kt
│ │ │ │ ├── GameThread.kt
│ │ │ │ ├── SoundManager.kt
│ │ │ │ ├── ControlButton.kt
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── Snake.kt
│ │ │ │ └── GameView.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── example
│ │ │ └── snake
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── example
│ │ └── snake
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
├── build.gradle.kts
└── build.gradle
├── README.md
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── .gitignore
├── rename_sounds.bat
├── settings.gradle
├── rename_sounds.sh
├── settings.gradle.kts
├── gradle.properties
├── gradlew.bat
└── gradlew
/.idea/.name:
--------------------------------------------------------------------------------
1 | Snake
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # snake_android
2 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | snake
3 |
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_0.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_0.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_1.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_1.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_2.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_2.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_3.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_3.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_4.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_4.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_5.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_5.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_6.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_6.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_7.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_7.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_8.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_8.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_9.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_9.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_a.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_a.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_b.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_b.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_c.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_c.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_d.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_d.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_e.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_e.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_f.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_f.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_g.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_g.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_h.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_h.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_i.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_i.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_j.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_j.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_k.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_k.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_l.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_l.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_m.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_m.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_n.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_n.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_o.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_o.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_p.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_p.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_q.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_q.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_r.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_r.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_s.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_s.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_t.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_t.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_u.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_u.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_v.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_v.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_w.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_w.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_x.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_x.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_y.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_y.wav
--------------------------------------------------------------------------------
/app/src/main/res/raw/sound_z.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/raw/sound_z.wav
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/java/com/example/snake/Direction.kt:
--------------------------------------------------------------------------------
1 | package com.example.snake
2 |
3 | enum class Direction {
4 | UP, DOWN, LEFT, RIGHT
5 | }
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey-wenjun/snake_android/main/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Jan 25 20:06:32 CST 2025
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/snake/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.example.snake.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/rename_sounds.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | cd app\src\main\res\raw
3 |
4 | REM 重命名数字文件
5 | for %%i in (0 1 2 3 4 5 6 7 8 9) do (
6 | if exist %%i.wav (
7 | ren "%%i.wav" "sound_%%i.wav"
8 | )
9 | )
10 |
11 | REM 重命名字母文件
12 | for %%i in (A B C D E F G H I J K L M N O P Q R S T U V W X Y Z) do (
13 | if exist %%i.wav (
14 | ren "%%i.wav" "sound_%%i.wav"
15 | )
16 | )
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 |
9 | dependencyResolutionManagement {
10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
11 | repositories {
12 | google()
13 | mavenCentral()
14 | }
15 | }
16 |
17 | rootProject.name = "Snake"
18 | include ':app'
--------------------------------------------------------------------------------
/rename_sounds.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd app/src/main/res/raw
3 |
4 | # 重命名数字文件
5 | for i in {0..9}; do
6 | if [ -f "$i.wav" ]; then
7 | mv "$i.wav" "sound_$i.wav"
8 | fi
9 | done
10 |
11 | # 重命名字母文件
12 | for letter in {A..Z}; do
13 | if [ -f "$letter.wav" ]; then
14 | # 转换为小写并重命名
15 | lower=$(echo "$letter" | tr '[:upper:]' '[:lower:]')
16 | mv "$letter.wav" "sound_$lower.wav"
17 | fi
18 | done
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/test/java/com/example/snake/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.snake
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
16 | repositories {
17 | google()
18 | mavenCentral()
19 | }
20 | }
21 |
22 | rootProject.name = "snake"
23 | include(":app")
24 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/example/snake/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.snake
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.example.snake", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/snake/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.example.snake.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.8.0"
3 | kotlin = "2.0.0"
4 | coreKtx = "1.10.1"
5 | junit = "4.13.2"
6 | junitVersion = "1.1.5"
7 | espressoCore = "3.5.1"
8 | lifecycleRuntimeKtx = "2.6.1"
9 | activityCompose = "1.8.0"
10 | composeBom = "2024.04.01"
11 |
12 | [libraries]
13 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
14 | junit = { group = "junit", name = "junit", version.ref = "junit" }
15 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
16 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
17 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
18 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
19 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
20 | androidx-ui = { group = "androidx.compose.ui", name = "ui" }
21 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
22 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
23 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
24 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
25 | androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
26 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
27 |
28 | [plugins]
29 | android-application = { id = "com.android.application", version.ref = "agp" }
30 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
31 | kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
32 |
33 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application)
3 | alias(libs.plugins.kotlin.android)
4 | alias(libs.plugins.kotlin.compose)
5 | }
6 |
7 | android {
8 | namespace = "com.example.snake"
9 | compileSdk = 35
10 |
11 | defaultConfig {
12 | applicationId = "com.example.snake"
13 | minSdk = 35
14 | targetSdk = 35
15 | versionCode = 1
16 | versionName = "1.0"
17 |
18 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
19 | }
20 |
21 | buildTypes {
22 | release {
23 | isMinifyEnabled = false
24 | proguardFiles(
25 | getDefaultProguardFile("proguard-android-optimize.txt"),
26 | "proguard-rules.pro"
27 | )
28 | }
29 | }
30 | compileOptions {
31 | sourceCompatibility = JavaVersion.VERSION_11
32 | targetCompatibility = JavaVersion.VERSION_11
33 | }
34 | kotlinOptions {
35 | jvmTarget = "11"
36 | }
37 | buildFeatures {
38 | compose = true
39 | }
40 | }
41 |
42 | dependencies {
43 |
44 | implementation(libs.androidx.core.ktx)
45 | implementation(libs.androidx.lifecycle.runtime.ktx)
46 | implementation(libs.androidx.activity.compose)
47 | implementation(platform(libs.androidx.compose.bom))
48 | implementation(libs.androidx.ui)
49 | implementation(libs.androidx.ui.graphics)
50 | implementation(libs.androidx.ui.tooling.preview)
51 | implementation(libs.androidx.material3)
52 | testImplementation(libs.junit)
53 | androidTestImplementation(libs.androidx.junit)
54 | androidTestImplementation(libs.androidx.espresso.core)
55 | androidTestImplementation(platform(libs.androidx.compose.bom))
56 | androidTestImplementation(libs.androidx.ui.test.junit4)
57 | debugImplementation(libs.androidx.ui.tooling)
58 | debugImplementation(libs.androidx.ui.test.manifest)
59 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/snake/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.example.snake.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.darkColorScheme
8 | import androidx.compose.material3.dynamicDarkColorScheme
9 | import androidx.compose.material3.dynamicLightColorScheme
10 | import androidx.compose.material3.lightColorScheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.platform.LocalContext
13 |
14 | private val DarkColorScheme = darkColorScheme(
15 | primary = Purple80,
16 | secondary = PurpleGrey80,
17 | tertiary = Pink80
18 | )
19 |
20 | private val LightColorScheme = lightColorScheme(
21 | primary = Purple40,
22 | secondary = PurpleGrey40,
23 | tertiary = Pink40
24 |
25 | /* Other default colors to override
26 | background = Color(0xFFFFFBFE),
27 | surface = Color(0xFFFFFBFE),
28 | onPrimary = Color.White,
29 | onSecondary = Color.White,
30 | onTertiary = Color.White,
31 | onBackground = Color(0xFF1C1B1F),
32 | onSurface = Color(0xFF1C1B1F),
33 | */
34 | )
35 |
36 | @Composable
37 | fun SnakeTheme(
38 | darkTheme: Boolean = isSystemInDarkTheme(),
39 | // Dynamic color is available on Android 12+
40 | dynamicColor: Boolean = true,
41 | content: @Composable () -> Unit
42 | ) {
43 | val colorScheme = when {
44 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
45 | val context = LocalContext.current
46 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
47 | }
48 |
49 | darkTheme -> DarkColorScheme
50 | else -> LightColorScheme
51 | }
52 |
53 | MaterialTheme(
54 | colorScheme = colorScheme,
55 | typography = Typography,
56 | content = content
57 | )
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/snake/Food.kt:
--------------------------------------------------------------------------------
1 | package com.example.snake
2 |
3 | import android.graphics.Canvas
4 | import android.graphics.Color
5 | import android.graphics.Paint
6 | import kotlin.random.Random
7 |
8 | class Food(
9 | private val screenWidth: Int,
10 | private val screenHeight: Int,
11 | val size: Float = 80f // Increased default size
12 | ) {
13 | var x: Float = 0f
14 | var y: Float = 0f
15 | var character: Char = 'A'
16 | private val color: Int
17 |
18 | private val paint = Paint().apply {
19 | style = Paint.Style.FILL
20 | isAntiAlias = true
21 | }
22 |
23 | private val textPaint = Paint().apply {
24 | textAlign = Paint.Align.CENTER
25 | textSize = size * 0.8f // Larger text size relative to food size
26 | isAntiAlias = true
27 | color = Color.BLACK
28 | isFakeBoldText = true // Make text bolder
29 | }
30 |
31 | init {
32 | // Generate brighter colors for better visibility
33 | color = Color.rgb(
34 | Random.nextInt(128, 256),
35 | Random.nextInt(128, 256),
36 | Random.nextInt(128, 256)
37 | )
38 | respawn()
39 | }
40 |
41 | fun respawn() {
42 | // Keep food away from edges
43 | val margin = size * 2
44 | x = Random.nextFloat() * (screenWidth - 2 * margin) + margin
45 | y = Random.nextFloat() * (screenHeight - 2 * margin) + margin
46 |
47 | // Generate random character (A-Z, a-z, 0-9)
48 | character = when (Random.nextInt(3)) {
49 | 0 -> Random.nextInt(26).let { 'A' + it } // Uppercase letters
50 | 1 -> Random.nextInt(26).let { 'a' + it } // Lowercase letters
51 | else -> Random.nextInt(10).let { '0' + it } // Numbers
52 | }
53 | }
54 |
55 | fun draw(canvas: Canvas) {
56 | // Draw glow effect
57 | paint.color = color
58 | for (i in 4 downTo 0) {
59 | paint.alpha = 50 - i * 10
60 | canvas.drawCircle(x, y, size * (1 + i * 0.1f), paint)
61 | }
62 |
63 | // Draw main circle
64 | paint.alpha = 255
65 | canvas.drawCircle(x, y, size, paint)
66 |
67 | // Draw character with shadow for better visibility
68 | textPaint.setShadowLayer(size * 0.1f, 0f, 0f, Color.WHITE)
69 | canvas.drawText(
70 | character.toString(),
71 | x,
72 | y + size * 0.3f, // Adjust Y position for better centering
73 | textPaint
74 | )
75 | }
76 | }
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'kotlin-android'
4 | }
5 |
6 | android {
7 | namespace 'com.example.snake'
8 | compileSdk 34
9 |
10 | defaultConfig {
11 | applicationId "com.example.snake"
12 | minSdk 26
13 | targetSdk 34
14 | versionCode 1
15 | versionName "1.0"
16 |
17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
18 | vectorDrawables {
19 | useSupportLibrary true
20 | }
21 | }
22 |
23 | buildTypes {
24 | release {
25 | minifyEnabled false
26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
27 | }
28 | }
29 |
30 | compileOptions {
31 | sourceCompatibility JavaVersion.VERSION_1_8
32 | targetCompatibility JavaVersion.VERSION_1_8
33 | }
34 |
35 | kotlinOptions {
36 | jvmTarget = '1.8'
37 | }
38 |
39 | buildFeatures {
40 | compose true
41 | }
42 |
43 | composeOptions {
44 | kotlinCompilerExtensionVersion '1.5.8'
45 | }
46 |
47 | packaging {
48 | resources {
49 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
50 | }
51 | }
52 |
53 | sourceSets {
54 | main {
55 | resources {
56 | srcDirs = ['src/main/resources']
57 | }
58 | }
59 | }
60 | }
61 |
62 | dependencies {
63 | implementation 'androidx.core:core-ktx:1.12.0'
64 | implementation 'androidx.appcompat:appcompat:1.6.1'
65 | implementation 'com.google.android.material:material:1.11.0'
66 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
67 |
68 | // Compose dependencies
69 | implementation platform('androidx.compose:compose-bom:2024.02.00')
70 | implementation 'androidx.activity:activity-compose:1.8.2'
71 | implementation 'androidx.compose.ui:ui'
72 | implementation 'androidx.compose.ui:ui-graphics'
73 | implementation 'androidx.compose.ui:ui-tooling-preview'
74 | implementation 'androidx.compose.material3:material3'
75 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
76 |
77 | // Audio dependencies
78 | implementation 'androidx.media:media:1.7.0'
79 |
80 | // Testing dependencies
81 | testImplementation 'junit:junit:4.13.2'
82 | androidTestImplementation 'androidx.test.ext:junit:1.1.5'
83 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
84 | androidTestImplementation platform('androidx.compose:compose-bom:2024.02.00')
85 | androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
86 | debugImplementation 'androidx.compose.ui:ui-tooling'
87 | debugImplementation 'androidx.compose.ui:ui-test-manifest'
88 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/snake/GameThread.kt:
--------------------------------------------------------------------------------
1 | package com.example.snake
2 |
3 | import android.graphics.Canvas
4 | import android.view.SurfaceHolder
5 | import android.util.Log
6 |
7 | class GameThread(
8 | private val surfaceHolder: SurfaceHolder,
9 | private val gameView: GameView
10 | ) : Thread() {
11 |
12 | companion object {
13 | private const val TAG = "GameThread"
14 | private const val TARGET_FPS = 60
15 | private const val TARGET_FRAME_TIME = (1000000000L / TARGET_FPS) // in nanoseconds
16 | }
17 |
18 | @Volatile
19 | private var running = false
20 |
21 | fun setRunning(isRunning: Boolean) {
22 | running = isRunning
23 | if (!isRunning) {
24 | interrupt()
25 | }
26 | }
27 |
28 | override fun run() {
29 | var lastTime = System.nanoTime()
30 | var currentTime: Long
31 | var deltaTime: Long
32 | var frameCount = 0
33 | var lastFPSTime = lastTime
34 |
35 | while (running && !isInterrupted) {
36 | currentTime = System.nanoTime()
37 | deltaTime = currentTime - lastTime
38 |
39 | if (deltaTime >= TARGET_FRAME_TIME) {
40 | var canvas: Canvas? = null
41 | try {
42 | canvas = surfaceHolder.lockCanvas()
43 | synchronized(surfaceHolder) {
44 | gameView.update()
45 | if (canvas != null) {
46 | gameView.draw(canvas)
47 | }
48 | }
49 |
50 | // FPS calculation
51 | frameCount++
52 | if (currentTime - lastFPSTime >= 1000000000L) { // Every second
53 | Log.d(TAG, "FPS: $frameCount")
54 | frameCount = 0
55 | lastFPSTime = currentTime
56 | }
57 |
58 | lastTime = currentTime
59 | } catch (e: Exception) {
60 | Log.e(TAG, "Error in game loop: ${e.message}")
61 | running = false
62 | } finally {
63 | try {
64 | canvas?.let { surfaceHolder.unlockCanvasAndPost(it) }
65 | } catch (e: Exception) {
66 | Log.e(TAG, "Error posting canvas: ${e.message}")
67 | running = false
68 | }
69 | }
70 | }
71 |
72 | // Sleep to save CPU
73 | val sleepTime = (TARGET_FRAME_TIME - (System.nanoTime() - currentTime)) / 1000000L
74 | if (sleepTime > 0) {
75 | try {
76 | sleep(sleepTime)
77 | } catch (e: InterruptedException) {
78 | Log.d(TAG, "Thread interrupted")
79 | running = false
80 | }
81 | }
82 | }
83 | }
84 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/snake/SoundManager.kt:
--------------------------------------------------------------------------------
1 | package com.example.snake
2 |
3 | import android.content.Context
4 | import android.media.AudioAttributes
5 | import android.media.SoundPool
6 | import android.util.Log
7 |
8 | class SoundManager(context: Context) {
9 | companion object {
10 | private const val TAG = "SoundManager"
11 | }
12 |
13 | private val soundPool: SoundPool
14 | private val soundMap = mutableMapOf()
15 |
16 | init {
17 | // Create AudioAttributes
18 | val audioAttributes = AudioAttributes.Builder()
19 | .setUsage(AudioAttributes.USAGE_GAME)
20 | .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
21 | .build()
22 |
23 | // Create SoundPool
24 | soundPool = SoundPool.Builder()
25 | .setMaxStreams(5)
26 | .setAudioAttributes(audioAttributes)
27 | .build()
28 |
29 | try {
30 | // Load number sounds (0-9)
31 | for (i in 0..9) {
32 | try {
33 | val resourceId = context.resources.getIdentifier(
34 | "sound_$i", "raw", context.packageName)
35 | if (resourceId != 0) {
36 | val soundId = soundPool.load(context, resourceId, 1)
37 | soundMap[i.toString()[0]] = soundId
38 | Log.d(TAG, "Loaded sound for number: $i")
39 | } else {
40 | Log.e(TAG, "Could not find sound resource for number: $i")
41 | }
42 | } catch (e: Exception) {
43 | Log.e(TAG, "Error loading sound for number $i: ${e.message}")
44 | }
45 | }
46 |
47 | // Load letter sounds (A-Z)
48 | for (c in 'A'..'Z') {
49 | try {
50 | val resourceId = context.resources.getIdentifier(
51 | "sound_${c.lowercase()}", "raw", context.packageName)
52 | if (resourceId != 0) {
53 | val soundId = soundPool.load(context, resourceId, 1)
54 | soundMap[c] = soundId
55 | soundMap[c.lowercaseChar()] = soundId // Use same sound for lowercase
56 | Log.d(TAG, "Loaded sound for letter: $c")
57 | } else {
58 | Log.e(TAG, "Could not find sound resource for letter: $c")
59 | }
60 | } catch (e: Exception) {
61 | Log.e(TAG, "Error loading sound for letter $c: ${e.message}")
62 | }
63 | }
64 |
65 | Log.d(TAG, "Successfully loaded ${soundMap.size} sounds")
66 | } catch (e: Exception) {
67 | Log.e(TAG, "Error initializing sounds: ${e.message}")
68 | }
69 | }
70 |
71 | fun playSound(character: Char) {
72 | try {
73 | soundMap[character]?.let { soundId ->
74 | val streamId = soundPool.play(soundId, 1f, 1f, 1, 0, 1f)
75 | if (streamId == 0) {
76 | Log.e(TAG, "Failed to play sound for character: $character")
77 | } else {
78 | Log.d(TAG, "Playing sound for character: $character (stream: $streamId)")
79 | }
80 | } ?: Log.e(TAG, "No sound loaded for character: $character")
81 | } catch (e: Exception) {
82 | Log.e(TAG, "Error playing sound for character $character: ${e.message}")
83 | }
84 | }
85 |
86 | fun release() {
87 | try {
88 | soundPool.release()
89 | Log.d(TAG, "SoundPool released")
90 | } catch (e: Exception) {
91 | Log.e(TAG, "Error releasing SoundPool: ${e.message}")
92 | }
93 | }
94 | }
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | xmlns:android
18 |
19 | ^$
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | xmlns:.*
29 |
30 | ^$
31 |
32 |
33 | BY_NAME
34 |
35 |
36 |
37 |
38 |
39 |
40 | .*:id
41 |
42 | http://schemas.android.com/apk/res/android
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | .*:name
52 |
53 | http://schemas.android.com/apk/res/android
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | name
63 |
64 | ^$
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | style
74 |
75 | ^$
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | .*
85 |
86 | ^$
87 |
88 |
89 | BY_NAME
90 |
91 |
92 |
93 |
94 |
95 |
96 | .*
97 |
98 | http://schemas.android.com/apk/res/android
99 |
100 |
101 | ANDROID_ATTRIBUTE_ORDER
102 |
103 |
104 |
105 |
106 |
107 |
108 | .*
109 |
110 | .*
111 |
112 |
113 | BY_NAME
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/snake/ControlButton.kt:
--------------------------------------------------------------------------------
1 | package com.example.snake
2 |
3 | import android.graphics.Canvas
4 | import android.graphics.Paint
5 | import android.graphics.Color
6 | import android.util.Log
7 | import kotlin.math.atan2
8 | import kotlin.math.sqrt
9 |
10 | class ControlButton(private val x: Float, private val y: Float, private val radius: Float) {
11 | companion object {
12 | private const val TAG = "ControlButton"
13 | }
14 |
15 | private val paint = Paint().apply {
16 | color = Color.GRAY
17 | style = Paint.Style.FILL
18 | alpha = 128
19 | isAntiAlias = true
20 | }
21 |
22 | private val borderPaint = Paint().apply {
23 | color = Color.WHITE
24 | style = Paint.Style.STROKE
25 | strokeWidth = 3f
26 | alpha = 80
27 | }
28 |
29 | private val arrowPaint = Paint().apply {
30 | color = Color.WHITE
31 | style = Paint.Style.FILL
32 | strokeWidth = 3f
33 | alpha = 120
34 | }
35 |
36 | private var snake: Snake? = null
37 |
38 | fun setSnake(snake: Snake) {
39 | this.snake = snake
40 | Log.d(TAG, "Snake reference set: $snake")
41 | }
42 |
43 | fun resetDirection() {
44 | snake?.setDirection(null)
45 | Log.d(TAG, "Direction reset")
46 | }
47 |
48 | fun draw(canvas: Canvas) {
49 | try {
50 | // Draw main circle
51 | canvas.drawCircle(x, y, radius, paint)
52 | canvas.drawCircle(x, y, radius, borderPaint)
53 |
54 | // Draw direction arrows
55 | val arrowSize = radius * 0.3f
56 | val arrowDist = radius * 0.5f
57 |
58 | // Draw arrows in all four directions
59 | canvas.drawPath(createArrowPath(x, y - arrowDist, arrowSize, Direction.UP), arrowPaint)
60 | canvas.drawPath(createArrowPath(x, y + arrowDist, arrowSize, Direction.DOWN), arrowPaint)
61 | canvas.drawPath(createArrowPath(x - arrowDist, y, arrowSize, Direction.LEFT), arrowPaint)
62 | canvas.drawPath(createArrowPath(x + arrowDist, y, arrowSize, Direction.RIGHT), arrowPaint)
63 | } catch (e: Exception) {
64 | Log.e(TAG, "Error drawing control button: ${e.message}", e)
65 | }
66 | }
67 |
68 | private fun createArrowPath(centerX: Float, centerY: Float, size: Float, direction: Direction): android.graphics.Path {
69 | val path = android.graphics.Path()
70 |
71 | when (direction) {
72 | Direction.UP -> {
73 | path.moveTo(centerX, centerY - size)
74 | path.lineTo(centerX - size, centerY + size)
75 | path.lineTo(centerX + size, centerY + size)
76 | }
77 | Direction.DOWN -> {
78 | path.moveTo(centerX, centerY + size)
79 | path.lineTo(centerX - size, centerY - size)
80 | path.lineTo(centerX + size, centerY - size)
81 | }
82 | Direction.LEFT -> {
83 | path.moveTo(centerX - size, centerY)
84 | path.lineTo(centerX + size, centerY - size)
85 | path.lineTo(centerX + size, centerY + size)
86 | }
87 | Direction.RIGHT -> {
88 | path.moveTo(centerX + size, centerY)
89 | path.lineTo(centerX - size, centerY - size)
90 | path.lineTo(centerX - size, centerY + size)
91 | }
92 | }
93 | path.close()
94 | return path
95 | }
96 |
97 | fun updateDirection(touchX: Float, touchY: Float) {
98 | if (!isPressed(touchX, touchY)) {
99 | return
100 | }
101 |
102 | try {
103 | // Calculate angle between touch point and button center
104 | val dx = touchX - x
105 | val dy = touchY - y
106 | val angle = Math.toDegrees(atan2(dy.toDouble(), dx.toDouble())).toFloat()
107 |
108 | // Determine direction based on angle
109 | val direction = when {
110 | angle in -45.0..45.0 -> Direction.RIGHT
111 | angle in 45.0..135.0 -> Direction.DOWN
112 | angle in -135.0..-45.0 -> Direction.UP
113 | else -> Direction.LEFT
114 | }
115 |
116 | Log.d(TAG, "Setting direction: $direction, angle: $angle")
117 | snake?.setDirection(direction)
118 | } catch (e: Exception) {
119 | Log.e(TAG, "Error updating direction: ${e.message}", e)
120 | }
121 | }
122 |
123 | fun isPressed(touchX: Float, touchY: Float): Boolean {
124 | val dx = touchX - x
125 | val dy = touchY - y
126 | val distance = sqrt(dx * dx + dy * dy)
127 | val result = distance <= radius * 1.2f // Slightly larger touch area
128 | if (result) {
129 | Log.d(TAG, "Button pressed at ($touchX, $touchY)")
130 | }
131 | return result
132 | }
133 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/snake/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.snake
2 |
3 | import android.os.Bundle
4 | import android.os.Handler
5 | import android.os.Looper
6 | import android.util.Log
7 | import android.view.WindowManager
8 | import android.widget.Toast
9 | import androidx.appcompat.app.AlertDialog
10 | import androidx.appcompat.app.AppCompatActivity
11 | import androidx.core.view.WindowCompat
12 | import androidx.core.view.WindowInsetsCompat
13 | import androidx.core.view.WindowInsetsControllerCompat
14 |
15 | class MainActivity : AppCompatActivity() {
16 | companion object {
17 | private const val TAG = "MainActivity"
18 | private const val GAME_TIME_LIMIT_MS = 5 * 60 * 1000L // 5 minutes
19 | private const val WARNING_INTERVAL_MS = 60 * 1000L // Show warning every minute
20 | }
21 |
22 | private var gameView: GameView? = null
23 | private lateinit var windowInsetsController: WindowInsetsControllerCompat
24 | private val handler = Handler(Looper.getMainLooper())
25 | private var gameStartTime = 0L
26 | private var isGameLocked = false
27 |
28 | private val gameTimeRunnable = object : Runnable {
29 | override fun run() {
30 | val elapsedTime = System.currentTimeMillis() - gameStartTime
31 | val remainingTime = GAME_TIME_LIMIT_MS - elapsedTime
32 |
33 | when {
34 | remainingTime <= 0 -> {
35 | if (!isGameLocked) {
36 | lockGame()
37 | }
38 | }
39 | remainingTime <= WARNING_INTERVAL_MS -> {
40 | // Show warning in last minute
41 | showTimeWarning(remainingTime)
42 | handler.postDelayed(this, 15000) // Check every 15 seconds in last minute
43 | }
44 | else -> {
45 | // Regular interval check
46 | handler.postDelayed(this, WARNING_INTERVAL_MS)
47 | }
48 | }
49 | }
50 | }
51 |
52 | private fun showTimeWarning(remainingTime: Long) {
53 | val remainingSeconds = (remainingTime / 1000).toInt()
54 | Toast.makeText(
55 | this,
56 | "小朋友,还剩 ${remainingSeconds} 秒游戏时间哦,请注意休息~",
57 | Toast.LENGTH_SHORT
58 | ).show()
59 | }
60 |
61 | private fun lockGame() {
62 | isGameLocked = true
63 | gameView?.pauseGame()
64 |
65 | AlertDialog.Builder(this)
66 | .setTitle("温馨提醒")
67 | .setMessage("小朋友,你已经玩了5分钟啦!\n该休息一下,保护眼睛了哦!")
68 | .setCancelable(false)
69 | .setPositiveButton("好的,我去休息") { _, _ ->
70 | finish()
71 | }
72 | .show()
73 | }
74 |
75 | override fun onCreate(savedInstanceState: Bundle?) {
76 | super.onCreate(savedInstanceState)
77 | Log.d(TAG, "onCreate")
78 |
79 | try {
80 | // Enable edge-to-edge
81 | WindowCompat.setDecorFitsSystemWindows(window, false)
82 |
83 | // Configure window insets controller
84 | windowInsetsController = WindowInsetsControllerCompat(window, window.decorView).apply {
85 | // Hide system bars
86 | hide(WindowInsetsCompat.Type.systemBars())
87 | systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
88 | }
89 |
90 | // Keep screen on
91 | window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
92 |
93 | gameView = GameView(this)
94 | setContentView(gameView)
95 |
96 | // Start tracking game time
97 | gameStartTime = System.currentTimeMillis()
98 | handler.post(gameTimeRunnable)
99 |
100 | } catch (e: Exception) {
101 | Log.e(TAG, "Error in onCreate: ${e.message}", e)
102 | Toast.makeText(this, "游戏启动出错,请重试", Toast.LENGTH_SHORT).show()
103 | finish()
104 | }
105 | }
106 |
107 | override fun onWindowFocusChanged(hasFocus: Boolean) {
108 | super.onWindowFocusChanged(hasFocus)
109 | Log.d(TAG, "Window focus changed: $hasFocus")
110 | try {
111 | if (hasFocus) {
112 | // Re-hide system bars when focus is regained
113 | windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
114 | } else {
115 | gameView?.pauseGame()
116 | }
117 | } catch (e: Exception) {
118 | Log.e(TAG, "Error in onWindowFocusChanged: ${e.message}", e)
119 | }
120 | }
121 |
122 | override fun onPause() {
123 | Log.d(TAG, "onPause")
124 | try {
125 | super.onPause()
126 | gameView?.pauseGame()
127 | handler.removeCallbacks(gameTimeRunnable)
128 | } catch (e: Exception) {
129 | Log.e(TAG, "Error in onPause: ${e.message}", e)
130 | }
131 | }
132 |
133 | override fun onResume() {
134 | Log.d(TAG, "onResume")
135 | try {
136 | super.onResume()
137 | if (!isGameLocked) {
138 | // Add a slight delay before resuming to ensure surface is ready
139 | gameView?.postDelayed({
140 | gameView?.resumeGame()
141 | }, 100)
142 | handler.post(gameTimeRunnable)
143 | }
144 | } catch (e: Exception) {
145 | Log.e(TAG, "Error in onResume: ${e.message}", e)
146 | }
147 | }
148 |
149 | override fun onStop() {
150 | Log.d(TAG, "onStop")
151 | try {
152 | super.onStop()
153 | gameView?.pauseGame()
154 | handler.removeCallbacks(gameTimeRunnable)
155 | } catch (e: Exception) {
156 | Log.e(TAG, "Error in onStop: ${e.message}", e)
157 | }
158 | }
159 |
160 | override fun onDestroy() {
161 | Log.d(TAG, "onDestroy")
162 | try {
163 | super.onDestroy()
164 | handler.removeCallbacks(gameTimeRunnable)
165 | gameView?.cleanup()
166 | gameView = null
167 | } catch (e: Exception) {
168 | Log.e(TAG, "Error in onDestroy: ${e.message}", e)
169 | }
170 | }
171 |
172 | override fun finish() {
173 | Log.d(TAG, "finish")
174 | try {
175 | gameView?.cleanup()
176 | handler.removeCallbacks(gameTimeRunnable)
177 | super.finish()
178 | } catch (e: Exception) {
179 | Log.e(TAG, "Error in finish: ${e.message}", e)
180 | }
181 | }
182 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/snake/Snake.kt:
--------------------------------------------------------------------------------
1 | package com.example.snake
2 |
3 | import android.graphics.Canvas
4 | import android.graphics.Color
5 | import android.graphics.Paint
6 | import android.graphics.PointF
7 | import kotlin.math.abs
8 | import kotlin.math.cos
9 | import kotlin.math.sin
10 | import kotlin.math.sqrt
11 | import kotlin.random.Random
12 | import java.util.Collections
13 | import java.util.concurrent.CopyOnWriteArrayList
14 | import android.util.Log
15 |
16 | class Snake(
17 | startX: Float,
18 | startY: Float,
19 | private val screenWidth: Float,
20 | private val screenHeight: Float,
21 | private val segmentSize: Float = 80f
22 | ) {
23 | private val segments = CopyOnWriteArrayList()
24 | private var direction = Direction.RIGHT
25 | private var nextDirection = direction
26 | private val speed = segmentSize * 0.15f
27 | private var isMoving = false
28 | private val color = Color.rgb(
29 | Random.nextInt(128, 256),
30 | Random.nextInt(128, 256),
31 | Random.nextInt(128, 256)
32 | )
33 |
34 | private val bodyPaint = Paint().apply {
35 | color = this@Snake.color
36 | style = Paint.Style.FILL
37 | isAntiAlias = true
38 | }
39 |
40 | private val eyePaint = Paint().apply {
41 | color = Color.WHITE
42 | style = Paint.Style.FILL
43 | isAntiAlias = true
44 | }
45 |
46 | private val pupilPaint = Paint().apply {
47 | color = Color.BLACK
48 | style = Paint.Style.FILL
49 | isAntiAlias = true
50 | }
51 |
52 | private val namePaint = Paint().apply {
53 | color = Color.WHITE
54 | textSize = segmentSize * 0.6f
55 | textAlign = Paint.Align.CENTER
56 | isAntiAlias = true
57 | }
58 |
59 | private val playerName = "超级无敌小芒果"
60 |
61 | private var lastPosition = PointF(0f, 0f)
62 | private var hasMoved = false
63 |
64 | init {
65 | setStartPosition(startX, startY)
66 | }
67 |
68 | fun setStartPosition(x: Float, y: Float) {
69 | segments.clear()
70 | for (i in 0..4) {
71 | segments.add(PointF(x - i * segmentSize, y))
72 | }
73 | }
74 |
75 | fun setDirection(newDirection: Direction?) {
76 | if (newDirection == null) {
77 | isMoving = false
78 | return
79 | }
80 |
81 | isMoving = true
82 | when (newDirection) {
83 | Direction.UP -> if (direction != Direction.DOWN) nextDirection = newDirection
84 | Direction.DOWN -> if (direction != Direction.UP) nextDirection = newDirection
85 | Direction.LEFT -> if (direction != Direction.RIGHT) nextDirection = newDirection
86 | Direction.RIGHT -> if (direction != Direction.LEFT) nextDirection = newDirection
87 | }
88 | }
89 |
90 | fun move() {
91 | if (!isMoving) {
92 | hasMoved = false
93 | return
94 | }
95 |
96 | direction = nextDirection
97 | val head = segments.first()
98 | val newHead = PointF(head.x, head.y)
99 | lastPosition.set(head.x, head.y)
100 |
101 | // Calculate new position
102 | when (direction) {
103 | Direction.UP -> newHead.y -= speed
104 | Direction.DOWN -> newHead.y += speed
105 | Direction.LEFT -> newHead.x -= speed
106 | Direction.RIGHT -> newHead.x += speed
107 | }
108 |
109 | // Wrap around screen edges smoothly
110 | when {
111 | newHead.x < -segmentSize -> newHead.x = screenWidth + segmentSize
112 | newHead.x > screenWidth + segmentSize -> newHead.x = -segmentSize
113 | newHead.y < -segmentSize -> newHead.y = screenHeight + segmentSize
114 | newHead.y > screenHeight + segmentSize -> newHead.y = -segmentSize
115 | }
116 |
117 | // Only update position if moved significantly
118 | val dx = abs(newHead.x - lastPosition.x)
119 | val dy = abs(newHead.y - lastPosition.y)
120 | hasMoved = (dx > 0.1f || dy > 0.1f)
121 |
122 | if (hasMoved) {
123 | // Add new head
124 | segments.add(0, newHead)
125 | // Remove tail
126 | if (segments.size > 1) {
127 | segments.removeAt(segments.size - 1)
128 | }
129 | }
130 | }
131 |
132 | fun hasMovedSinceLastCheck(): Boolean {
133 | val moved = hasMoved
134 | hasMoved = false
135 | return moved
136 | }
137 |
138 | fun grow() {
139 | val tail = segments.last()
140 | repeat(2) {
141 | segments.add(PointF(tail.x, tail.y))
142 | }
143 | }
144 |
145 | private fun drawEyes(canvas: Canvas, head: PointF) {
146 | val eyeRadius = segmentSize / 6
147 | val pupilRadius = eyeRadius / 2
148 | val eyeDistance = segmentSize / 3
149 |
150 | // Calculate eye positions based on direction
151 | val eyePositions = when (direction) {
152 | Direction.RIGHT -> {
153 | val x = head.x + segmentSize / 4
154 | EyePositions(
155 | x, head.y - eyeDistance/2, // left eye
156 | x, head.y + eyeDistance/2 // right eye
157 | )
158 | }
159 | Direction.LEFT -> {
160 | val x = head.x - segmentSize / 4
161 | EyePositions(
162 | x, head.y - eyeDistance/2, // left eye
163 | x, head.y + eyeDistance/2 // right eye
164 | )
165 | }
166 | Direction.UP -> {
167 | val y = head.y - segmentSize / 4
168 | EyePositions(
169 | head.x - eyeDistance/2, y, // left eye
170 | head.x + eyeDistance/2, y // right eye
171 | )
172 | }
173 | Direction.DOWN -> {
174 | val y = head.y + segmentSize / 4
175 | EyePositions(
176 | head.x - eyeDistance/2, y, // left eye
177 | head.x + eyeDistance/2, y // right eye
178 | )
179 | }
180 | }
181 |
182 | // Draw eyes (white part)
183 | canvas.drawCircle(eyePositions.leftEyeX, eyePositions.leftEyeY, eyeRadius, eyePaint)
184 | canvas.drawCircle(eyePositions.rightEyeX, eyePositions.rightEyeY, eyeRadius, eyePaint)
185 |
186 | // Draw pupils (black part)
187 | // Add slight offset to pupils based on direction to give more character
188 | val pupilOffset = eyeRadius / 3
189 | val pupilPositions = when (direction) {
190 | Direction.RIGHT -> PupilPositions(
191 | eyePositions.leftEyeX + pupilOffset, eyePositions.leftEyeY,
192 | eyePositions.rightEyeX + pupilOffset, eyePositions.rightEyeY
193 | )
194 | Direction.LEFT -> PupilPositions(
195 | eyePositions.leftEyeX - pupilOffset, eyePositions.leftEyeY,
196 | eyePositions.rightEyeX - pupilOffset, eyePositions.rightEyeY
197 | )
198 | Direction.UP -> PupilPositions(
199 | eyePositions.leftEyeX, eyePositions.leftEyeY - pupilOffset,
200 | eyePositions.rightEyeX, eyePositions.rightEyeY - pupilOffset
201 | )
202 | Direction.DOWN -> PupilPositions(
203 | eyePositions.leftEyeX, eyePositions.leftEyeY + pupilOffset,
204 | eyePositions.rightEyeX, eyePositions.rightEyeY + pupilOffset
205 | )
206 | }
207 |
208 | canvas.drawCircle(pupilPositions.leftPupilX, pupilPositions.leftPupilY, pupilRadius, pupilPaint)
209 | canvas.drawCircle(pupilPositions.rightPupilX, pupilPositions.rightPupilY, pupilRadius, pupilPaint)
210 | }
211 |
212 | private data class EyePositions(
213 | val leftEyeX: Float,
214 | val leftEyeY: Float,
215 | val rightEyeX: Float,
216 | val rightEyeY: Float
217 | )
218 |
219 | private data class PupilPositions(
220 | val leftPupilX: Float,
221 | val leftPupilY: Float,
222 | val rightPupilX: Float,
223 | val rightPupilY: Float
224 | )
225 |
226 | fun draw(canvas: Canvas) {
227 | // Create a snapshot of the segments to avoid concurrent modification
228 | val currentSegments = segments.toList()
229 |
230 | // Draw body segments with gradient alpha
231 | currentSegments.forEachIndexed { index, segment ->
232 | val alpha = 255 - (index * 255 / currentSegments.size).coerceAtMost(200)
233 | bodyPaint.alpha = alpha
234 | canvas.drawCircle(segment.x, segment.y, segmentSize / 2, bodyPaint)
235 | }
236 |
237 | // Draw eyes on the head
238 | if (currentSegments.isNotEmpty()) {
239 | val head = currentSegments.first()
240 | drawEyes(canvas, head)
241 |
242 | // Draw player name above the head
243 | canvas.drawText(playerName, head.x, head.y - segmentSize, namePaint)
244 | }
245 | }
246 |
247 | fun checkFoodCollision(food: Food): Boolean {
248 | val head = segments.first()
249 | val dx = head.x - food.x
250 | val dy = head.y - food.y
251 | val distance = sqrt(dx * dx + dy * dy)
252 |
253 | // Less strict collision detection
254 | val collisionThreshold = (segmentSize + food.size) * 0.5f
255 | val collision = distance < collisionThreshold
256 |
257 | if (collision) {
258 | Log.d("SnakeGame", "检测到碰撞!距离: ${"%.2f".format(distance)}, 阈值: ${"%.2f".format(collisionThreshold)}")
259 | }
260 |
261 | return collision
262 | }
263 |
264 | fun getSegments(): List = segments.toList() // Return a copy of the segments list
265 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/snake/GameView.kt:
--------------------------------------------------------------------------------
1 | package com.example.snake
2 |
3 | import android.content.Context
4 | import android.graphics.Canvas
5 | import android.graphics.Color
6 | import android.graphics.Paint
7 | import android.graphics.PointF
8 | import android.util.AttributeSet
9 | import android.util.Log
10 | import android.view.MotionEvent
11 | import android.view.SurfaceHolder
12 | import android.view.SurfaceView
13 | import android.view.WindowManager
14 | import kotlin.random.Random
15 |
16 | class GameView @JvmOverloads constructor(
17 | context: Context,
18 | attrs: AttributeSet? = null,
19 | defStyleAttr: Int = 0
20 | ) : SurfaceView(context, attrs, defStyleAttr), SurfaceHolder.Callback {
21 |
22 | companion object {
23 | private const val TAG = "GameView"
24 | private const val FOOD_COUNT = 8
25 | private const val MIN_FOOD_DISTANCE = 150f
26 | private const val SEGMENT_SIZE = 80f
27 | }
28 |
29 | private var thread: GameThread? = null
30 | private lateinit var snake: Snake
31 | private val foodItems = mutableListOf()
32 | private lateinit var controlButton: ControlButton
33 | private var score = 0
34 | private val soundManager = SoundManager(context)
35 | private var isGamePaused = false
36 | private var gameStartTime = System.currentTimeMillis()
37 | private var lastDrawTime = 0L
38 |
39 | private val scorePaint = Paint().apply {
40 | color = Color.WHITE
41 | textSize = 50f
42 | textAlign = Paint.Align.RIGHT
43 | }
44 |
45 | private val timePaint = Paint().apply {
46 | color = Color.WHITE
47 | textSize = 50f
48 | textAlign = Paint.Align.CENTER
49 | }
50 |
51 | init {
52 | try {
53 | holder.addCallback(this)
54 | isFocusable = true
55 | isClickable = true
56 | isFocusableInTouchMode = true
57 | setBackgroundColor(Color.BLACK)
58 | } catch (e: Exception) {
59 | Log.e(TAG, "Error in init: ${e.message}")
60 | e.printStackTrace()
61 | }
62 | }
63 |
64 | fun pauseGame() {
65 | Log.d(TAG, "Pausing game")
66 | isGamePaused = true
67 | cleanupThread()
68 | }
69 |
70 | fun resumeGame() {
71 | Log.d(TAG, "Resuming game")
72 | isGamePaused = false
73 | if (holder.surface?.isValid == true) {
74 | startNewThread()
75 | }
76 | }
77 |
78 | private fun startNewThread() {
79 | Log.d(TAG, "Starting new game thread")
80 | cleanupThread() // Ensure old thread is cleaned up
81 | thread = GameThread(holder, this).apply {
82 | setRunning(true)
83 | start()
84 | }
85 | }
86 |
87 | override fun surfaceCreated(holder: SurfaceHolder) {
88 | Log.d(TAG, "Surface created: width=$width, height=$height")
89 |
90 | try {
91 | // Initialize game objects if not already initialized
92 | if (!::snake.isInitialized) {
93 | snake = Snake(width / 2f, height / 2f, width.toFloat(), height.toFloat(), SEGMENT_SIZE)
94 |
95 | // Initialize control button
96 | val buttonRadius = height / 5f
97 | val margin = buttonRadius * 0.2f
98 | val buttonX = buttonRadius + margin
99 | val buttonY = height - (buttonRadius + margin)
100 | controlButton = ControlButton(buttonX, buttonY, buttonRadius)
101 | controlButton.setSnake(snake)
102 |
103 | initializeFood()
104 |
105 | // Reset game start time when initializing new game
106 | gameStartTime = System.currentTimeMillis()
107 | }
108 |
109 | resumeGame()
110 | } catch (e: Exception) {
111 | Log.e(TAG, "Error in surfaceCreated: ${e.message}", e)
112 | }
113 | }
114 |
115 | private fun initializeFood() {
116 | foodItems.clear()
117 | val snakeSegments = snake.getSegments()
118 |
119 | repeat(FOOD_COUNT) {
120 | var food: Food
121 | var validPosition: Boolean
122 | var attempts = 0
123 |
124 | do {
125 | food = Food(width, height, SEGMENT_SIZE) // Use same size as snake segments
126 | validPosition = true
127 |
128 | // Check distance from snake
129 | for (segment in snakeSegments) {
130 | if (distance(food.x, food.y, segment.x, segment.y) < MIN_FOOD_DISTANCE) {
131 | validPosition = false
132 | break
133 | }
134 | }
135 |
136 | // Check distance from other food items
137 | for (existingFood in foodItems) {
138 | if (distance(food.x, food.y, existingFood.x, existingFood.y) < MIN_FOOD_DISTANCE) {
139 | validPosition = false
140 | break
141 | }
142 | }
143 |
144 | attempts++
145 | if (attempts > 100) break // Prevent infinite loop
146 |
147 | } while (!validPosition)
148 |
149 | if (validPosition) {
150 | foodItems.add(food)
151 | }
152 | }
153 | }
154 |
155 | private fun distance(x1: Float, y1: Float, x2: Float, y2: Float): Float {
156 | val dx = x1 - x2
157 | val dy = y1 - y2
158 | return kotlin.math.sqrt(dx * dx + dy * dy)
159 | }
160 |
161 | override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
162 | Log.d(TAG, "Surface changed: width=$width, height=$height")
163 | }
164 |
165 | override fun surfaceDestroyed(holder: SurfaceHolder) {
166 | Log.d(TAG, "Surface destroyed")
167 | pauseGame()
168 | }
169 |
170 | override fun onDetachedFromWindow() {
171 | Log.d(TAG, "Detached from window")
172 | super.onDetachedFromWindow()
173 | cleanup()
174 | }
175 |
176 | private fun cleanupThread() {
177 | Log.d(TAG, "Cleaning up game thread")
178 | thread?.let { currentThread ->
179 | try {
180 | currentThread.setRunning(false)
181 |
182 | // Wait for the thread to die with timeout
183 | var retry = true
184 | var timeoutCount = 0
185 | while (retry && timeoutCount < 3) {
186 | try {
187 | currentThread.join(200) // Increased timeout to 200ms
188 | retry = false
189 | } catch (e: InterruptedException) {
190 | timeoutCount++
191 | Log.w(TAG, "Thread join interrupted, attempt $timeoutCount")
192 | }
193 | }
194 |
195 | if (retry) {
196 | Log.w(TAG, "Thread cleanup timed out after $timeoutCount attempts")
197 | // Force interrupt if join failed
198 | currentThread.interrupt()
199 | } else {
200 | Log.d(TAG, "Thread cleanup completed successfully")
201 | }
202 | } catch (e: Exception) {
203 | Log.e(TAG, "Error cleaning up thread: ${e.message}")
204 | e.printStackTrace()
205 | } finally {
206 | thread = null
207 | }
208 | }
209 | }
210 |
211 | fun cleanup() {
212 | Log.d(TAG, "Cleaning up GameView resources")
213 | try {
214 | isGamePaused = true
215 | cleanupThread()
216 | soundManager.release()
217 | } catch (e: Exception) {
218 | Log.e(TAG, "Error during cleanup: ${e.message}")
219 | }
220 | }
221 |
222 | fun update() {
223 | if (isGamePaused) {
224 | return
225 | }
226 |
227 | try {
228 | synchronized(this) {
229 | // Store previous position to check if actually moved
230 | val prevHead = snake.getSegments().firstOrNull()?.let { PointF(it.x, it.y) }
231 |
232 | snake.move()
233 |
234 | // Only check collisions if snake actually moved
235 | val currentHead = snake.getSegments().firstOrNull()
236 | if (prevHead != null && currentHead != null) {
237 | val dx = prevHead.x - currentHead.x
238 | val dy = prevHead.y - currentHead.y
239 | val moved = (dx != 0f || dy != 0f)
240 |
241 | if (moved) {
242 | Log.d("SnakeGame", "蛇移动了!")
243 | checkCollisions()
244 | } else {
245 | Log.d("SnakeGame", "蛇没有移动")
246 | }
247 | }
248 |
249 | // Respawn food if there are too few
250 | while (foodItems.size < FOOD_COUNT) {
251 | val food = Food(width, height, SEGMENT_SIZE)
252 | if (isValidFoodPosition(food)) {
253 | foodItems.add(food)
254 | }
255 | }
256 | }
257 | } catch (e: Exception) {
258 | Log.e(TAG, "Error in update: ${e.message}")
259 | e.printStackTrace()
260 | try {
261 | pauseGame()
262 | } catch (e2: Exception) {
263 | Log.e(TAG, "Error while trying to pause game after error: ${e2.message}")
264 | e2.printStackTrace()
265 | }
266 | }
267 | }
268 |
269 | private fun isValidFoodPosition(food: Food): Boolean {
270 | // Check if food is too close to edges
271 | if (food.x < MIN_FOOD_DISTANCE || food.x > width - MIN_FOOD_DISTANCE ||
272 | food.y < MIN_FOOD_DISTANCE || food.y > height - MIN_FOOD_DISTANCE) {
273 | return false
274 | }
275 |
276 | // Check distance from snake
277 | for (segment in snake.getSegments()) {
278 | if (distance(food.x, food.y, segment.x, segment.y) < MIN_FOOD_DISTANCE) {
279 | return false
280 | }
281 | }
282 |
283 | // Check distance from other food items
284 | for (existingFood in foodItems) {
285 | if (distance(food.x, food.y, existingFood.x, existingFood.y) < MIN_FOOD_DISTANCE) {
286 | return false
287 | }
288 | }
289 |
290 | return true
291 | }
292 |
293 | override fun draw(canvas: Canvas) {
294 | if (canvas == null) {
295 | Log.e(TAG, "Canvas is null in draw")
296 | return
297 | }
298 |
299 | try {
300 | super.draw(canvas)
301 |
302 | // Draw game objects
303 | synchronized(this) {
304 | snake.draw(canvas)
305 | foodItems.forEach { it.draw(canvas) }
306 | controlButton.draw(canvas)
307 |
308 | // Draw score in top-right corner
309 | canvas.drawText("当前积分: $score", width - 20f, 60f, scorePaint)
310 |
311 | // Draw game time in top-center
312 | val gameTimeSeconds = ((System.currentTimeMillis() - gameStartTime) / 1000).toInt()
313 | val minutes = gameTimeSeconds / 60
314 | val seconds = gameTimeSeconds % 60
315 | val timeText = String.format("游戏时间: %02d:%02d", minutes, seconds)
316 | canvas.drawText(timeText, width / 2f, 60f, timePaint)
317 | }
318 | } catch (e: Exception) {
319 | Log.e(TAG, "Error in draw: ${e.message}")
320 | e.printStackTrace()
321 | }
322 | }
323 |
324 | private fun checkCollisions() {
325 | // Check food collisions
326 | val iterator = foodItems.iterator()
327 | while (iterator.hasNext()) {
328 | val food = iterator.next()
329 | if (snake.checkFoodCollision(food)) {
330 | val points = when (food.character) {
331 | in 'A'..'Z' -> 30
332 | in 'a'..'z' -> 20
333 | in '0'..'9' -> 10
334 | else -> 0
335 | }
336 | score += points
337 | Log.d("SnakeGame", "吃到食物了!字符: ${food.character}, 得分: $points")
338 |
339 | snake.grow()
340 | soundManager.playSound(food.character)
341 | iterator.remove()
342 | break
343 | }
344 | }
345 | }
346 |
347 | override fun onTouchEvent(event: MotionEvent): Boolean {
348 | if (isGamePaused) {
349 | return false
350 | }
351 |
352 | try {
353 | synchronized(this) {
354 | when (event.action) {
355 | MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
356 | controlButton.updateDirection(event.x, event.y)
357 | postInvalidate() // Use postInvalidate for thread safety
358 | return true
359 | }
360 | MotionEvent.ACTION_UP -> {
361 | controlButton.resetDirection()
362 | postInvalidate()
363 | return true
364 | }
365 | else -> return super.onTouchEvent(event)
366 | }
367 | }
368 | } catch (e: Exception) {
369 | Log.e(TAG, "Error handling touch event: ${e.message}")
370 | e.printStackTrace()
371 | }
372 | return super.onTouchEvent(event)
373 | }
374 | }
--------------------------------------------------------------------------------