├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── font
│ │ │ │ ├── clearsans_bold.ttf
│ │ │ │ ├── clearsans_thin.ttf
│ │ │ │ ├── clearsans_italic.ttf
│ │ │ │ ├── clearsans_light.ttf
│ │ │ │ ├── clearsans_medium.ttf
│ │ │ │ ├── clearsans_regular.ttf
│ │ │ │ ├── clearsans_bolditalic.ttf
│ │ │ │ └── clearsans_mediumitalic.ttf
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_rounded.png
│ │ │ │ ├── ic_launcher_adaptive_back.png
│ │ │ │ ├── ic_launcher_adaptive_fore.png
│ │ │ │ ├── ic_launcher_rounded_adaptive_back.png
│ │ │ │ └── ic_launcher_rounded_adaptive_fore.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_rounded.png
│ │ │ │ ├── ic_launcher_adaptive_back.png
│ │ │ │ ├── ic_launcher_adaptive_fore.png
│ │ │ │ ├── ic_launcher_rounded_adaptive_back.png
│ │ │ │ └── ic_launcher_rounded_adaptive_fore.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_rounded.png
│ │ │ │ ├── ic_launcher_adaptive_back.png
│ │ │ │ ├── ic_launcher_adaptive_fore.png
│ │ │ │ ├── ic_launcher_rounded_adaptive_back.png
│ │ │ │ └── ic_launcher_rounded_adaptive_fore.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_rounded.png
│ │ │ │ ├── ic_launcher_adaptive_back.png
│ │ │ │ ├── ic_launcher_adaptive_fore.png
│ │ │ │ ├── ic_launcher_rounded_adaptive_back.png
│ │ │ │ └── ic_launcher_rounded_adaptive_fore.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_rounded.png
│ │ │ │ ├── ic_launcher_adaptive_back.png
│ │ │ │ ├── ic_launcher_adaptive_fore.png
│ │ │ │ ├── ic_launcher_rounded_adaptive_back.png
│ │ │ │ └── ic_launcher_rounded_adaptive_fore.png
│ │ │ ├── values
│ │ │ │ ├── themes.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── strings.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_rounded.xml
│ │ │ ├── xml
│ │ │ │ ├── backup_rules.xml
│ │ │ │ └── data_extraction_rules.xml
│ │ │ ├── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ └── drawable
│ │ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ │ └── info
│ │ │ │ └── degirona
│ │ │ │ └── compose2048
│ │ │ │ ├── Utils.kt
│ │ │ │ ├── ui
│ │ │ │ ├── theme
│ │ │ │ │ ├── Color.kt
│ │ │ │ │ ├── Shape.kt
│ │ │ │ │ ├── Theme.kt
│ │ │ │ │ └── Type.kt
│ │ │ │ ├── board
│ │ │ │ │ ├── Layer.kt
│ │ │ │ │ ├── BoardRenderer.kt
│ │ │ │ │ ├── BoardScope.kt
│ │ │ │ │ ├── BoardBackground.kt
│ │ │ │ │ └── Board.kt
│ │ │ │ ├── data
│ │ │ │ │ └── ViewModels.kt
│ │ │ │ ├── game
│ │ │ │ │ ├── SubHeader.kt
│ │ │ │ │ ├── GameScreen.kt
│ │ │ │ │ ├── Header.kt
│ │ │ │ │ └── GameScreenViewModel.kt
│ │ │ │ └── tile
│ │ │ │ │ ├── Tile.kt
│ │ │ │ │ └── TileRenderer.kt
│ │ │ │ ├── game
│ │ │ │ ├── StorageManager.kt
│ │ │ │ ├── GameModels.kt
│ │ │ │ └── GameManager.kt
│ │ │ │ └── MainActivity.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── info
│ │ │ └── degirona
│ │ │ └── compose2048
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── info
│ │ └── degirona
│ │ └── compose2048
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
├── config
│ ├── detekt-compose.yml
│ └── detekt.yml
├── build.gradle
└── detekt-baseline.xml
├── compose-2048.gif
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── README.md
├── settings.gradle
├── LICENSE.txt
├── gradle.properties
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/compose-2048.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/compose-2048.gif
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/font/clearsans_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/font/clearsans_bold.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/clearsans_thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/font/clearsans_thin.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/clearsans_italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/font/clearsans_italic.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/clearsans_light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/font/clearsans_light.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/clearsans_medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/font/clearsans_medium.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/clearsans_regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/font/clearsans_regular.ttf
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/font/clearsans_bolditalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/font/clearsans_bolditalic.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/clearsans_mediumitalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/font/clearsans_mediumitalic.ttf
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_rounded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_rounded.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_rounded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_rounded.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_rounded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_rounded.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | .idea
4 | /local.properties
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 | local.properties
11 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_rounded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_rounded.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_rounded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_rounded.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_back.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_fore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_fore.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_back.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_fore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_adaptive_fore.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_back.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_back.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_adaptive_fore.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This project is about a take on the famous [2048 game](https://github.com/gabrielecirulli/2048) implemented using Jetpack Compose.
2 |
3 |
4 |
5 | 
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_rounded_adaptive_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_rounded_adaptive_back.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_rounded_adaptive_fore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_rounded_adaptive_fore.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_rounded_adaptive_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_rounded_adaptive_back.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_rounded_adaptive_fore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_rounded_adaptive_fore.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_rounded_adaptive_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_rounded_adaptive_back.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_rounded_adaptive_fore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_rounded_adaptive_fore.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_rounded_adaptive_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_rounded_adaptive_back.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_rounded_adaptive_fore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_rounded_adaptive_fore.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_rounded_adaptive_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_rounded_adaptive_back.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_rounded_adaptive_fore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manuel-martos/compose-2048/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_rounded_adaptive_fore.png
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Nov 01 22:21:43 CET 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/java/info/degirona/compose2048/Utils.kt:
--------------------------------------------------------------------------------
1 | package info.degirona.compose2048
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.platform.LocalContext
5 |
6 | @Composable
7 | fun Int.resolve() = LocalContext.current.getString(this)
8 |
9 | fun List.toArrayList(): ArrayList = ArrayList(this)
10 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_rounded.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/java/info/degirona/compose2048/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package info.degirona.compose2048.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Primary = Color(0xFFbfab9d)
6 | val PrimaryVariant = Color(0xFF776E65)
7 | val Secondary = Color(0XFFF0E4D9)
8 | val Background = Color(0xFFFAF8EE)
9 | val OnSurface = Color(0xFF776E65)
10 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "Compose 2048"
16 | include ':app'
17 |
--------------------------------------------------------------------------------
/app/src/main/java/info/degirona/compose2048/ui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package info.degirona.compose2048.ui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val Shapes = Shapes(
8 | small = RoundedCornerShape(4.dp),
9 | medium = RoundedCornerShape(4.dp),
10 | large = RoundedCornerShape(0.dp)
11 | )
12 |
--------------------------------------------------------------------------------
/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/test/java/info/degirona/compose2048/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package info.degirona.compose2048
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 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Compose 2048
3 | 2048
4 |
5 | Score
6 | Best
7 |
8 | New game
9 |
10 | You win!
11 | Game over!
12 | Try again
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/java/info/degirona/compose2048/ui/board/Layer.kt:
--------------------------------------------------------------------------------
1 | package info.degirona.compose2048.ui.board
2 |
3 | sealed class Layer {
4 | object CellLayer : Layer()
5 | object AnimationLayer : Layer()
6 | object MergeLayer : Layer()
7 | object PopupLayer : Layer()
8 |
9 | companion object {
10 | fun Layer.toZIndex() =
11 | when (this) {
12 | CellLayer -> 0.0f
13 | AnimationLayer -> 1.0f
14 | MergeLayer -> 2.0f
15 | PopupLayer -> 3.0f
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/main/java/info/degirona/compose2048/ui/data/ViewModels.kt:
--------------------------------------------------------------------------------
1 | package info.degirona.compose2048.ui.data
2 |
3 | import androidx.compose.runtime.Immutable
4 | import androidx.compose.runtime.Stable
5 |
6 | @Stable
7 | data class BoardModel(
8 | val newTiles: List = emptyList(),
9 | val staticTiles: List = emptyList(),
10 | val swipedTiles: List = emptyList(),
11 | val mergedTiles: List = emptyList(),
12 | )
13 |
14 | @Immutable
15 | data class TilePosition(
16 | val row: Float,
17 | val col: Float,
18 | )
19 |
20 | @Immutable
21 | data class TileModel(
22 | val id: String,
23 | val curValue: Int,
24 | val curPosition: TilePosition,
25 | val prevPosition: TilePosition? = null,
26 | )
27 |
--------------------------------------------------------------------------------
/app/src/main/java/info/degirona/compose2048/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package info.degirona.compose2048.ui.theme
2 |
3 | import androidx.compose.material.MaterialTheme
4 | import androidx.compose.material.lightColors
5 | import androidx.compose.runtime.Composable
6 |
7 | private val LightColorPalette = lightColors(
8 | primary = Primary,
9 | primaryVariant = PrimaryVariant,
10 | secondary = Secondary,
11 | background = Background,
12 | surface = Background,
13 | onSurface = OnSurface,
14 | onBackground = OnSurface,
15 | )
16 |
17 | @Composable
18 | fun Compose2048Theme(content: @Composable () -> Unit) {
19 | MaterialTheme(
20 | colors = LightColorPalette,
21 | typography = Typography,
22 | shapes = Shapes,
23 | content = content
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/app/src/androidTest/java/info/degirona/compose2048/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package info.degirona.compose2048
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("info.degirona.compose2048", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/info/degirona/compose2048/ui/game/SubHeader.kt:
--------------------------------------------------------------------------------
1 | package info.degirona.compose2048.ui.game
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.material.Button
7 | import androidx.compose.material.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import info.degirona.compose2048.R
11 | import info.degirona.compose2048.resolve
12 |
13 | @Composable
14 | fun SubHeader(
15 | onResetClicked: () -> Unit,
16 | modifier: Modifier = Modifier
17 | ) {
18 | Row(
19 | modifier = modifier.fillMaxWidth(),
20 | horizontalArrangement = Arrangement.End,
21 | ) {
22 | Button(
23 | onClick = onResetClicked
24 | ) {
25 | Text(R.string.sub_header_new_game.resolve())
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/main/java/info/degirona/compose2048/ui/board/BoardRenderer.kt:
--------------------------------------------------------------------------------
1 | package info.degirona.compose2048.ui.board
2 |
3 | import androidx.compose.runtime.Composable
4 | import info.degirona.compose2048.ui.data.BoardModel
5 | import info.degirona.compose2048.ui.tile.TileRenderer
6 | import info.degirona.compose2048.ui.tile.TileRendererInstance
7 |
8 | interface BoardRenderer {
9 | @Composable
10 | fun BoardScope.Render(boardModel: BoardModel)
11 | }
12 |
13 | internal object BoardRendererInstance :
14 | BoardRenderer,
15 | TileRenderer by TileRendererInstance {
16 |
17 | @Composable
18 | override fun BoardScope.Render(boardModel: BoardModel) {
19 | boardModel.staticTiles.forEach { tileModel -> RenderStaticTile(tileModel) }
20 | boardModel.swipedTiles.forEach { tileModel -> RenderSwipedTile(tileModel) }
21 | boardModel.mergedTiles.forEach { tileModel -> RenderMergedTile(tileModel) }
22 | boardModel.newTiles.forEach { tileModel -> RenderNewTile(tileModel) }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/info/degirona/compose2048/game/StorageManager.kt:
--------------------------------------------------------------------------------
1 | package info.degirona.compose2048.game
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 |
6 | interface StorageManager {
7 | fun getBestScore(): Int
8 | fun setBestScore(score: Int)
9 | }
10 |
11 | class StorageManagerImpl(
12 | context: Context,
13 | ) : StorageManager {
14 | private val sharedPreferences: SharedPreferences
15 |
16 | init {
17 | sharedPreferences = context.getSharedPreferences(GAME_DATA, Context.MODE_PRIVATE)
18 | }
19 |
20 | override fun getBestScore(): Int =
21 | sharedPreferences.getInt(BEST_SCORE_KEY, 0)
22 |
23 | override fun setBestScore(bestScore: Int) {
24 | with (sharedPreferences.edit()) {
25 | putInt(BEST_SCORE_KEY, bestScore)
26 | apply()
27 | }
28 | }
29 |
30 | private companion object {
31 | const val GAME_DATA = "gameData"
32 | const val BEST_SCORE_KEY = "bestScoreKey"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright 2022 Manuel Martos
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7 | of the Software, and to permit persons to whom the Software is furnished to do
8 | so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
--------------------------------------------------------------------------------
/app/src/main/java/info/degirona/compose2048/ui/board/BoardScope.kt:
--------------------------------------------------------------------------------
1 | package info.degirona.compose2048.ui.board
2 |
3 | import androidx.compose.runtime.Stable
4 | import androidx.compose.ui.Modifier
5 | import androidx.compose.ui.layout.layout
6 | import info.degirona.compose2048.ui.board.Layer.Companion.toZIndex
7 | import kotlin.math.max
8 | import kotlin.math.roundToInt
9 |
10 | interface BoardScope {
11 | @Stable
12 | fun Modifier.boardCell(row: Float, col: Float, layer: Layer = Layer.CellLayer): Modifier
13 |
14 | val tileFraction: Float
15 | }
16 |
17 | class BoardScopeImpl(
18 | private val maxRows: Int,
19 | private val maxCols: Int,
20 | ) : BoardScope {
21 |
22 | @Stable
23 | override fun Modifier.boardCell(row: Float, col: Float, layer: Layer): Modifier =
24 | layout { measurable, constraints ->
25 | val placeable = measurable.measure(constraints)
26 | layout(placeable.width, placeable.height) {
27 | placeable.place(
28 | x = (placeable.width * col).roundToInt(),
29 | y = (placeable.height * row).roundToInt(),
30 | zIndex = layer.toZIndex()
31 | )
32 | }
33 | }
34 |
35 | override val tileFraction: Float get() = 1f / max(maxRows, maxCols)
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
16 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/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. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec: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/java/info/degirona/compose2048/ui/tile/Tile.kt:
--------------------------------------------------------------------------------
1 | package info.degirona.compose2048.ui.tile
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.shape.RoundedCornerShape
8 | import androidx.compose.material.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Alignment
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.graphics.Color
13 | import androidx.compose.ui.text.font.FontWeight
14 | import androidx.compose.ui.unit.TextUnit
15 | import androidx.compose.ui.unit.dp
16 |
17 | @Composable
18 | fun Tile(
19 | fraction: Float,
20 | value: String,
21 | color: Color,
22 | fontSize: TextUnit,
23 | backgroundColor: Color,
24 | modifier: Modifier = Modifier,
25 | ) {
26 | Box(
27 | modifier = modifier
28 | .fillMaxSize(fraction)
29 | .padding(tilePadding.dp)
30 | .background(backgroundColor, RoundedCornerShape(tileCornerRadius.dp)),
31 | contentAlignment = Alignment.Center,
32 | ) {
33 | Text(
34 | text = value,
35 | fontSize = fontSize,
36 | fontWeight = FontWeight.Bold,
37 | color = color,
38 | )
39 | }
40 | }
41 |
42 | private const val tilePadding = 4
43 | private const val tileCornerRadius = 4
44 |
--------------------------------------------------------------------------------
/app/src/main/java/info/degirona/compose2048/ui/board/BoardBackground.kt:
--------------------------------------------------------------------------------
1 | package info.degirona.compose2048.ui.board
2 |
3 | import androidx.compose.ui.Modifier
4 | import androidx.compose.ui.draw.drawBehind
5 | import androidx.compose.ui.geometry.CornerRadius
6 | import androidx.compose.ui.geometry.Offset
7 | import androidx.compose.ui.geometry.Size
8 | import androidx.compose.ui.graphics.Color
9 | import androidx.compose.ui.unit.dp
10 | import kotlin.math.min
11 |
12 | internal fun Modifier.boardBackground(
13 | maxRows: Int,
14 | maxCols: Int,
15 | ) = drawBehind {
16 | val fixFactorInPx = fixFactor.dp.toPx()
17 | val cellDim = min(size.width / maxCols, size.height / maxRows)
18 | for (row in 0 until maxRows) {
19 | for (col in 0 until maxCols) {
20 | val cellOffset =
21 | Offset(
22 | x = col * cellDim + fixFactorInPx,
23 | y = row * cellDim + fixFactorInPx
24 | )
25 | val cellSize = Size(
26 | width = cellDim - 2f * fixFactorInPx,
27 | height = cellDim - 2f * fixFactorInPx,
28 | )
29 | drawRoundRect(
30 | color = Color(bgTileColor),
31 | topLeft = cellOffset,
32 | size = cellSize,
33 | cornerRadius = CornerRadius(cornerRadius.dp.toPx())
34 | )
35 | }
36 | }
37 | }
38 |
39 | private const val fixFactor = 5
40 | private const val cornerRadius = 4
41 | private const val bgTileColor = 0xffcec1b2
42 |
--------------------------------------------------------------------------------
/app/config/detekt-compose.yml:
--------------------------------------------------------------------------------
1 | TwitterCompose:
2 | CompositionLocalAllowlist:
3 | active: true
4 | # You can optionally define a list of CompositionLocals that are allowed here
5 | # allowedCompositionLocals: LocalSomething,LocalSomethingElse
6 | CompositionLocalNaming:
7 | active: true
8 | ContentEmitterReturningValues:
9 | active: true
10 | # You can optionally add your own composables here
11 | # contentEmitters: MyComposable,MyOtherComposable
12 | ModifierComposable:
13 | active: true
14 | ModifierMissing:
15 | active: true
16 | ModifierReused:
17 | active: true
18 | ModifierWithoutDefault:
19 | active: true
20 | MultipleEmitters:
21 | active: true
22 | # You can optionally add your own composables here
23 | # contentEmitters: MyComposable,MyOtherComposable
24 | MutableParams:
25 | active: true
26 | ComposableNaming:
27 | active: true
28 | # You can optionally disable the checks in this rule for regex matches against the composable name (e.g. molecule presenters)
29 | # allowedComposableFunctionNames: .*Presenter,.*MoleculePresenter
30 | ComposableParamOrder:
31 | active: true
32 | PreviewNaming:
33 | active: true
34 | PreviewPublic:
35 | active: true
36 | # You can optionally disable that only previews with @PreviewParameter are flagged
37 | # previewPublicOnlyIfParams: false
38 | RememberMissing:
39 | active: true
40 | UnstableCollections:
41 | active: true
42 | ViewModelForwarding:
43 | active: true
44 | ViewModelInjection:
45 | active: true
46 |
--------------------------------------------------------------------------------
/app/src/main/java/info/degirona/compose2048/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package info.degirona.compose2048
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.material.MaterialTheme
10 | import androidx.compose.material.Surface
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.tooling.preview.Preview
14 | import androidx.compose.ui.unit.dp
15 | import info.degirona.compose2048.ui.game.GameScreen
16 | import info.degirona.compose2048.ui.theme.Compose2048Theme
17 |
18 | class MainActivity : ComponentActivity() {
19 | override fun onCreate(savedInstanceState: Bundle?) {
20 | super.onCreate(savedInstanceState)
21 | setContent {
22 | Compose2048Theme {
23 | // A surface container using the 'background' color from the theme
24 | Surface(
25 | modifier = Modifier
26 | .fillMaxSize(),
27 | color = MaterialTheme.colors.background
28 | ) {
29 | Box(
30 | Modifier.padding(16.dp)
31 | ) {
32 | GameScreen()
33 | }
34 | }
35 | }
36 | }
37 | }
38 | }
39 |
40 | @Preview
41 | @Composable
42 | fun GameScreenPreview() {
43 | GameScreen()
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/info/degirona/compose2048/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package info.degirona.compose2048.ui.theme
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.Font
6 | import androidx.compose.ui.text.font.FontFamily
7 | import androidx.compose.ui.text.font.FontStyle
8 | import androidx.compose.ui.text.font.FontWeight
9 | import androidx.compose.ui.unit.sp
10 | import info.degirona.compose2048.R
11 |
12 | val fonts = FontFamily(
13 | Font(R.font.clearsans_regular),
14 | Font(R.font.clearsans_bold, weight = FontWeight.Bold),
15 | Font(R.font.clearsans_light, weight = FontWeight.Light),
16 | Font(R.font.clearsans_medium, weight = FontWeight.Medium),
17 | Font(R.font.clearsans_thin, weight = FontWeight.Thin),
18 | Font(R.font.clearsans_italic, style = FontStyle.Italic),
19 | Font(R.font.clearsans_bolditalic, weight = FontWeight.Bold, style = FontStyle.Italic),
20 | Font(R.font.clearsans_mediumitalic, weight = FontWeight.Medium, style = FontStyle.Italic),
21 | )
22 |
23 | // Set of Material typography styles to start with
24 | val Typography = Typography(
25 | defaultFontFamily = fonts,
26 | h3 = TextStyle(
27 | fontFamily = fonts,
28 | fontWeight = FontWeight.Bold,
29 | fontSize = 50.sp
30 | ),
31 | body1 = TextStyle(
32 | fontFamily = fonts,
33 | fontWeight = FontWeight.Normal,
34 | fontSize = 24.sp,
35 | lineHeight = 24.sp,
36 | ),
37 | body2 = TextStyle(
38 | fontFamily = fonts,
39 | fontWeight = FontWeight.Bold,
40 | fontSize = 12.sp,
41 | lineHeight = 12.sp,
42 | ),
43 | )
44 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/java/info/degirona/compose2048/game/GameModels.kt:
--------------------------------------------------------------------------------
1 | package info.degirona.compose2048.game
2 |
3 | import info.degirona.compose2048.toArrayList
4 | import kotlin.random.Random
5 |
6 | data class Position(
7 | val x: Int,
8 | val y: Int,
9 | )
10 |
11 | data class Tile(
12 | val id: String,
13 | val value: Int,
14 | val curPosition: Position,
15 | val prevPosition: Position? = null,
16 | val mergedFrom: Pair? = null
17 | ) {
18 | val x: Int get() = curPosition.x
19 | val y: Int get() = curPosition.y
20 | }
21 |
22 | data class Grid(
23 | val size: Int,
24 | val cells: ArrayList> =
25 | (0 until size).map {
26 | (0 until size).map {
27 | null
28 | }.toArrayList()
29 | }.toArrayList()
30 | ) {
31 | fun insertTile(tile: Tile) {
32 | cells[tile.x][tile.y] = tile
33 | }
34 |
35 | fun removeTile(tile: Tile) {
36 | cells[tile.x][tile.y] = null
37 | }
38 |
39 | fun randomAvailableCell(): Position? {
40 | val availableCells = availableCells()
41 | return if (availableCells.isNotEmpty()) {
42 | availableCells[Random.nextInt(availableCells.size)]
43 | } else null
44 | }
45 |
46 | fun isCellAvailable(position: Position) =
47 | !isCellOccupied(position)
48 |
49 | fun isCellOccupied(position: Position) =
50 | getCellContent(position) != null
51 |
52 | private fun getCellContent(position: Position): Tile? =
53 | if (withinBounds(position)) {
54 | cells[position.x][position.y]
55 | } else {
56 | null
57 | }
58 |
59 | fun withinBounds(position: Position) =
60 | position.x >= 0 && position.x < this.size &&
61 | position.y >= 0 && position.y < this.size;
62 |
63 | fun availableCells(): List =
64 | (0 until size).flatMap { x ->
65 | (0 until size)
66 | .filter { y -> cells[x][y] == null }
67 | .map { y -> Position(x, y) }
68 | }
69 |
70 | fun cellContent(position: Position): Tile? =
71 | if (withinBounds(position)) {
72 | cells[position.x][position.y]
73 | } else {
74 | null
75 | }
76 |
77 | fun areCellsAvailable(): Boolean =
78 | availableCells().isNotEmpty()
79 | }
80 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'io.gitlab.arturbosch.detekt' version '1.21.0'
5 | }
6 |
7 | android {
8 | namespace 'info.degirona.compose2048'
9 | compileSdk 33
10 |
11 | defaultConfig {
12 | applicationId "info.degirona.compose2048"
13 | minSdk 28
14 | targetSdk 33
15 | versionCode 1
16 | versionName "1.0"
17 |
18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
19 | vectorDrawables {
20 | useSupportLibrary true
21 | }
22 | }
23 |
24 | buildTypes {
25 | release {
26 | minifyEnabled false
27 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
28 | }
29 | }
30 | compileOptions {
31 | sourceCompatibility JavaVersion.VERSION_1_8
32 | targetCompatibility JavaVersion.VERSION_1_8
33 | }
34 | kotlinOptions {
35 | jvmTarget = '1.8'
36 | }
37 | buildFeatures {
38 | compose true
39 | }
40 | composeOptions {
41 | kotlinCompilerExtensionVersion '1.3.2'
42 | }
43 | packagingOptions {
44 | resources {
45 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
46 | }
47 | }
48 | }
49 |
50 | dependencies {
51 | implementation 'androidx.activity:activity-compose:1.6.1'
52 | implementation "androidx.compose.animation:animation-core:$compose_ui_version"
53 | implementation 'androidx.compose.material:material:1.3.1'
54 | implementation "androidx.compose.ui:ui:$compose_ui_version"
55 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
56 | implementation 'androidx.core:core-ktx:1.9.0'
57 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
58 | implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1'
59 | testImplementation 'junit:junit:4.13.2'
60 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
61 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
62 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
63 | debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"
64 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
65 | detektPlugins "com.twitter.compose.rules:detekt:0.0.24"
66 | }
67 |
68 | detekt {
69 | buildUponDefaultConfig = true
70 | allRules = false
71 | config = files("$projectDir/config/detekt.yml", "$projectDir/config/detekt-compose.yml")
72 | baseline = file("$projectDir/detekt-baseline.xml")
73 |
74 | reports {
75 | xml {
76 | required.set(true)
77 | outputLocation.set(file("build/reports/detekt.xml"))
78 | }
79 |
80 | txt.required.set(false)
81 | html.required.set(false)
82 | sarif.required.set(false)
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/main/java/info/degirona/compose2048/ui/game/GameScreen.kt:
--------------------------------------------------------------------------------
1 | package info.degirona.compose2048.ui.game
2 |
3 | import androidx.compose.foundation.gestures.detectDragGestures
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.MutableState
9 | import androidx.compose.runtime.mutableStateOf
10 | import androidx.compose.runtime.remember
11 | import androidx.compose.ui.Alignment
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.geometry.Offset
14 | import androidx.compose.ui.input.pointer.pointerInput
15 | import androidx.compose.ui.platform.LocalContext
16 | import androidx.compose.ui.tooling.preview.Preview
17 | import androidx.lifecycle.viewmodel.compose.viewModel
18 | import info.degirona.compose2048.game.GameManager
19 | import info.degirona.compose2048.game.StorageManagerImpl
20 | import info.degirona.compose2048.ui.board.Board
21 | import info.degirona.compose2048.ui.board.BoardRendererInstance
22 | import info.degirona.compose2048.ui.theme.Compose2048Theme
23 |
24 | @Composable
25 | fun GameScreen(
26 | modifier: Modifier = Modifier,
27 | size: Int = 4,
28 | viewModel: GameScreenViewModel = viewModel(
29 | factory = GameScreenViewModelFactory(
30 | GameManager(
31 | size,
32 | StorageManagerImpl(LocalContext.current)
33 | )
34 | )
35 | ),
36 | ) {
37 | Box(
38 | modifier = modifier
39 | ) {
40 | Column(
41 | modifier = Modifier.align(Alignment.TopCenter)
42 | ) {
43 | Header(
44 | score = viewModel.score,
45 | bestScore = viewModel.bestScore,
46 | )
47 | SubHeader(
48 | onResetClicked = { viewModel.restartGame() }
49 | )
50 | }
51 |
52 | Board(
53 | maxRows = size,
54 | maxCols = size,
55 | onTryAgainClicked = { viewModel.restartGame() },
56 | modifier = Modifier
57 | .align(Alignment.Center)
58 | .fillMaxSize()
59 | .dragDetector(
60 | enabled = !viewModel.won && !viewModel.over,
61 | dragOffset = rememberDragOffset(),
62 | onDragFinished = { dragOffset -> viewModel.applyDragGesture(dragOffset) }
63 | ),
64 | won = viewModel.won,
65 | over = viewModel.over,
66 | ) {
67 | BoardRendererInstance.apply {
68 | Render(viewModel.boardModel)
69 | }
70 | }
71 | }
72 | }
73 |
74 | internal fun Modifier.dragDetector(
75 | enabled: Boolean,
76 | dragOffset: MutableState,
77 | onDragFinished: (Offset) -> Unit,
78 | ) = pointerInput(Unit) {
79 | if (enabled) {
80 | detectDragGestures(
81 | onDragStart = { dragOffset.value = Offset(0f, 0f) },
82 | onDragEnd = { onDragFinished(dragOffset.value) }
83 | ) { change, dragAmount ->
84 | change.consume()
85 | dragOffset.value += Offset(dragAmount.x, dragAmount.y)
86 | }
87 | }
88 | }
89 |
90 | @Composable
91 | internal fun rememberDragOffset() = remember { mutableStateOf(Offset(0f, 0f)) }
92 |
93 | @Preview
94 | @Composable
95 | private fun GamePreview() {
96 | Compose2048Theme {
97 | GameScreen()
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/app/src/main/java/info/degirona/compose2048/ui/game/Header.kt:
--------------------------------------------------------------------------------
1 | package info.degirona.compose2048.ui.game
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.RowScope
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.requiredHeight
11 | import androidx.compose.foundation.layout.requiredWidth
12 | import androidx.compose.foundation.layout.wrapContentWidth
13 | import androidx.compose.foundation.shape.RoundedCornerShape
14 | import androidx.compose.material.MaterialTheme
15 | import androidx.compose.material.Text
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.compose.ui.text.font.FontWeight
21 | import androidx.compose.ui.tooling.preview.Preview
22 | import androidx.compose.ui.unit.dp
23 | import info.degirona.compose2048.R
24 | import info.degirona.compose2048.resolve
25 | import info.degirona.compose2048.ui.theme.Compose2048Theme
26 |
27 | @Composable
28 | fun Header(
29 | score: Int,
30 | bestScore: Int,
31 | modifier: Modifier = Modifier
32 | ) {
33 | Row(
34 | modifier = modifier.fillMaxWidth()
35 | ) {
36 | HeaderTitle()
37 | HeaderPanel(title = R.string.header_score.resolve(), value = score.toString())
38 | Spacer(modifier = Modifier.requiredWidth(headerSpacing.dp))
39 | HeaderPanel(title = R.string.header_best.resolve(), value = bestScore.toString())
40 | }
41 | }
42 |
43 | @Composable
44 | fun RowScope.HeaderTitle(
45 | modifier: Modifier = Modifier
46 | ) {
47 | Text(
48 | modifier = modifier.weight(1f),
49 | text = R.string.app_name_short.resolve(),
50 | style = MaterialTheme.typography.h3,
51 | color = Color(titleColor)
52 | )
53 | }
54 |
55 | @Composable
56 | fun HeaderPanel(
57 | title: String,
58 | value: String,
59 | modifier: Modifier = Modifier
60 | ) {
61 | Box(
62 | modifier = modifier
63 | .wrapContentWidth(unbounded = true)
64 | .requiredHeight(panelHeight.dp)
65 | .background(Color(panelColor), RoundedCornerShape(panelCornerRadius.dp))
66 | .padding(horizontal = panelPadding.dp),
67 | contentAlignment = Alignment.BottomCenter
68 | ) {
69 | Text(
70 | text = title.uppercase(),
71 | style = MaterialTheme.typography.body2,
72 | color = Color(panelTitleColor),
73 | modifier = Modifier.padding(bottom = panelTitlePaddingBottom.dp)
74 | )
75 | Text(
76 | text = value,
77 | style = MaterialTheme.typography.body1,
78 | fontWeight = FontWeight.Bold,
79 | color = Color(panelValueColor)
80 | )
81 | }
82 | }
83 |
84 | @Preview
85 | @Composable
86 | private fun HeaderPreview() {
87 | Compose2048Theme {
88 | Header(score = 128, bestScore = 65536)
89 | }
90 | }
91 |
92 | private const val headerSpacing = 4
93 | private const val panelHeight = 48
94 | private const val panelPadding = 8
95 | private const val panelCornerRadius = 2
96 | private const val panelColor = 0xffbfab9d
97 | private const val panelTitlePaddingBottom = 28
98 | private const val panelTitleColor = 0xfff0e4d9
99 | private const val panelValueColor = 0xffffffff
100 | private const val titleColor = 0xff776e65
101 |
--------------------------------------------------------------------------------
/app/detekt-baseline.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | MagicNumber:Color.kt$0XFFF0E4D9
6 | MagicNumber:Color.kt$0xFF776E65
7 | MagicNumber:Color.kt$0xFFFAF8EE
8 | MagicNumber:Color.kt$0xFFbfab9d
9 | MagicNumber:GameManager.kt$GameManager$0.9f
10 | MagicNumber:GameManager.kt$GameManager$2048
11 | MagicNumber:GameManager.kt$GameManager$4
12 | MagicNumber:Layer.kt$Layer.Companion$3.0f
13 | MagicNumber:TileRenderer.kt$TileRendererInstance$0.5f
14 | MagicNumber:TileRenderer.kt$TileRendererInstance$0xff3c3a32
15 | MagicNumber:TileRenderer.kt$TileRendererInstance$0xff776e65
16 | MagicNumber:TileRenderer.kt$TileRendererInstance$0xffedc22e
17 | MagicNumber:TileRenderer.kt$TileRendererInstance$0xffedc53f
18 | MagicNumber:TileRenderer.kt$TileRendererInstance$0xffedc850
19 | MagicNumber:TileRenderer.kt$TileRendererInstance$0xffedcc61
20 | MagicNumber:TileRenderer.kt$TileRendererInstance$0xffedcf72
21 | MagicNumber:TileRenderer.kt$TileRendererInstance$0xffede0c8
22 | MagicNumber:TileRenderer.kt$TileRendererInstance$0xffeee4da
23 | MagicNumber:TileRenderer.kt$TileRendererInstance$0xfff2b179
24 | MagicNumber:TileRenderer.kt$TileRendererInstance$0xfff59563
25 | MagicNumber:TileRenderer.kt$TileRendererInstance$0xfff65e3b
26 | MagicNumber:TileRenderer.kt$TileRendererInstance$0xfff67c5f
27 | MagicNumber:TileRenderer.kt$TileRendererInstance$0xfff8f6f2
28 | MagicNumber:TileRenderer.kt$TileRendererInstance$1.2f
29 | MagicNumber:TileRenderer.kt$TileRendererInstance$1024
30 | MagicNumber:TileRenderer.kt$TileRendererInstance$128
31 | MagicNumber:TileRenderer.kt$TileRendererInstance$16
32 | MagicNumber:TileRenderer.kt$TileRendererInstance$2048
33 | MagicNumber:TileRenderer.kt$TileRendererInstance$256
34 | MagicNumber:TileRenderer.kt$TileRendererInstance$32
35 | MagicNumber:TileRenderer.kt$TileRendererInstance$4
36 | MagicNumber:TileRenderer.kt$TileRendererInstance$512
37 | MagicNumber:TileRenderer.kt$TileRendererInstance$64
38 | MagicNumber:TileRenderer.kt$TileRendererInstance$8
39 | NestedBlockDepth:GameManager.kt$GameManager$fun move(direction: Direction)
40 | NestedBlockDepth:GameManager.kt$GameManager$private fun tileMatchesAvailable(): Boolean
41 | TopLevelPropertyNaming:Board.kt$private const val backgroundColor = 0xffbfab9dL
42 | TopLevelPropertyNaming:Board.kt$private const val cornerRadius = 4
43 | TopLevelPropertyNaming:Board.kt$private const val innerPadding = 4
44 | TopLevelPropertyNaming:Board.kt$private const val popupBackgroundOverColor = 0x80ffffffL
45 | TopLevelPropertyNaming:Board.kt$private const val popupBackgroundWinColor = 0xa0edc301L
46 | TopLevelPropertyNaming:Board.kt$private const val popupMessageFontSize = 56
47 | TopLevelPropertyNaming:Board.kt$private const val popupMessageOverColor = 0xff776e65L
48 | TopLevelPropertyNaming:Board.kt$private const val popupMessageWinColor = 0xffffffffL
49 | TopLevelPropertyNaming:BoardBackground.kt$private const val bgTileColor = 0xffcec1b2
50 | TopLevelPropertyNaming:BoardBackground.kt$private const val cornerRadius = 4
51 | TopLevelPropertyNaming:BoardBackground.kt$private const val fixFactor = 5
52 | TopLevelPropertyNaming:Header.kt$private const val headerSpacing = 4
53 | TopLevelPropertyNaming:Header.kt$private const val panelColor = 0xffbfab9d
54 | TopLevelPropertyNaming:Header.kt$private const val panelCornerRadius = 2
55 | TopLevelPropertyNaming:Header.kt$private const val panelHeight = 48
56 | TopLevelPropertyNaming:Header.kt$private const val panelPadding = 8
57 | TopLevelPropertyNaming:Header.kt$private const val panelTitleColor = 0xfff0e4d9
58 | TopLevelPropertyNaming:Header.kt$private const val panelTitlePaddingBottom = 28
59 | TopLevelPropertyNaming:Header.kt$private const val panelValueColor = 0xffffffff
60 | TopLevelPropertyNaming:Header.kt$private const val titleColor = 0xff776e65
61 | TopLevelPropertyNaming:Tile.kt$private const val tileCornerRadius = 4
62 | TopLevelPropertyNaming:Tile.kt$private const val tilePadding = 4
63 | UnusedPrivateMember:Board.kt$@Preview @Composable private fun BoardEmptyPreview()
64 | UnusedPrivateMember:Board.kt$@Preview @Composable private fun BoardGameOverPreview()
65 | UnusedPrivateMember:Board.kt$@Preview @Composable private fun BoardWonPreview()
66 | UnusedPrivateMember:GameScreen.kt$@Preview @Composable private fun GamePreview()
67 | UnusedPrivateMember:Header.kt$@Preview @Composable private fun HeaderPreview()
68 |
69 |
70 |
--------------------------------------------------------------------------------
/app/src/main/java/info/degirona/compose2048/ui/board/Board.kt:
--------------------------------------------------------------------------------
1 | package info.degirona.compose2048.ui.board
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.animation.fadeIn
5 | import androidx.compose.animation.fadeOut
6 | import androidx.compose.foundation.background
7 | import androidx.compose.foundation.layout.Arrangement
8 | import androidx.compose.foundation.layout.Box
9 | import androidx.compose.foundation.layout.Column
10 | import androidx.compose.foundation.layout.aspectRatio
11 | import androidx.compose.foundation.layout.fillMaxSize
12 | import androidx.compose.foundation.layout.padding
13 | import androidx.compose.foundation.shape.RoundedCornerShape
14 | import androidx.compose.material.Button
15 | import androidx.compose.material.Text
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.compose.ui.text.font.FontWeight
21 | import androidx.compose.ui.tooling.preview.Preview
22 | import androidx.compose.ui.unit.dp
23 | import androidx.compose.ui.unit.sp
24 | import androidx.compose.ui.zIndex
25 | import info.degirona.compose2048.R
26 | import info.degirona.compose2048.resolve
27 | import info.degirona.compose2048.ui.board.Layer.Companion.toZIndex
28 | import info.degirona.compose2048.ui.theme.Compose2048Theme
29 |
30 | @Composable
31 | fun Board(
32 | won: Boolean,
33 | over: Boolean,
34 | onTryAgainClicked: () -> Unit,
35 | modifier: Modifier = Modifier,
36 | maxRows: Int = 4,
37 | maxCols: Int = 4,
38 | content: @Composable BoardScope.() -> Unit
39 | ) {
40 | Box(
41 | modifier = modifier
42 | .aspectRatio(1f)
43 | ) {
44 | Box(
45 | modifier = Modifier
46 | .aspectRatio(1f)
47 | .background(Color(backgroundColor), RoundedCornerShape(cornerRadius.dp))
48 | .padding(innerPadding.dp)
49 | .boardBackground(maxRows, maxCols),
50 | content = { BoardScopeImpl(maxRows, maxCols).content() }
51 | )
52 | AnimatedVisibility(
53 | visible = won || over,
54 | enter = fadeIn(),
55 | exit = fadeOut(),
56 | ) {
57 | Popup(
58 | text = won.toPopupMessage(),
59 | textColor = won.toPopupMessageColor(),
60 | backgroundColor = won.toPopupBackgroundColor(),
61 | onTryAgainClicked = onTryAgainClicked
62 | )
63 | }
64 | }
65 | }
66 |
67 | @Composable
68 | fun Popup(
69 | text: String,
70 | textColor: Color,
71 | backgroundColor: Color,
72 | onTryAgainClicked: () -> Unit,
73 | modifier: Modifier = Modifier,
74 | layer: Layer = Layer.PopupLayer,
75 | ) {
76 | Column(
77 | modifier = modifier
78 | .fillMaxSize()
79 | .zIndex(layer.toZIndex())
80 | .background(backgroundColor, RoundedCornerShape(cornerRadius.dp)),
81 | verticalArrangement = Arrangement.Center,
82 | horizontalAlignment = Alignment.CenterHorizontally,
83 | ) {
84 | Text(
85 | text = text,
86 | fontSize = popupMessageFontSize.sp,
87 | fontWeight = FontWeight.Bold,
88 | color = textColor,
89 | )
90 | Button(
91 | onClick = onTryAgainClicked
92 | ) {
93 | Text(R.string.popup_try_again.resolve())
94 | }
95 | }
96 | }
97 |
98 | @Composable
99 | private fun Boolean.toPopupMessage() =
100 | (if (this) R.string.popup_message_win else R.string.popup_message_game_over).resolve()
101 |
102 | private fun Boolean.toPopupMessageColor() =
103 | Color(if (this) popupMessageWinColor else popupMessageOverColor)
104 |
105 | private fun Boolean.toPopupBackgroundColor() =
106 | Color(if (this) popupBackgroundWinColor else popupBackgroundOverColor)
107 |
108 | @Preview
109 | @Composable
110 | private fun BoardEmptyPreview() {
111 | Compose2048Theme {
112 | Board(
113 | won = false,
114 | over = false,
115 | onTryAgainClicked = { },
116 | content = {}
117 | )
118 | }
119 | }
120 |
121 | @Preview
122 | @Composable
123 | private fun BoardWonPreview() {
124 | Compose2048Theme {
125 | Board(
126 | won = true,
127 | over = false,
128 | onTryAgainClicked = { },
129 | content = {}
130 | )
131 | }
132 | }
133 |
134 | @Preview
135 | @Composable
136 | private fun BoardGameOverPreview() {
137 | Compose2048Theme {
138 | Board(
139 | won = false,
140 | over = true,
141 | onTryAgainClicked = { },
142 | content = {}
143 | )
144 | }
145 | }
146 |
147 | private const val innerPadding = 4
148 | private const val cornerRadius = 4
149 | private const val backgroundColor = 0xffbfab9dL
150 | private const val popupMessageFontSize = 56
151 | private const val popupMessageWinColor = 0xffffffffL
152 | private const val popupMessageOverColor = 0xff776e65L
153 | private const val popupBackgroundWinColor = 0xa0edc301L
154 | private const val popupBackgroundOverColor = 0x80ffffffL
155 |
--------------------------------------------------------------------------------
/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/info/degirona/compose2048/game/GameManager.kt:
--------------------------------------------------------------------------------
1 | package info.degirona.compose2048.game
2 |
3 | import info.degirona.compose2048.game.Direction.Companion.toVector
4 | import java.util.UUID
5 | import kotlin.random.Random
6 |
7 | internal data class Vector(
8 | val x: Int,
9 | val y: Int,
10 | )
11 |
12 | internal data class Traversals(
13 | val x: IntProgression,
14 | val y: IntProgression,
15 | )
16 |
17 | internal data class FarthestPosition(
18 | val farthest: Position,
19 | val next: Position,
20 | )
21 |
22 | enum class Direction {
23 | Left, Right, Up, Down;
24 |
25 | companion object {
26 | internal fun Direction.toVector() =
27 | when (this) {
28 | Left -> Vector(x = -1, y = 0)
29 | Right -> Vector(x = 1, y = 0)
30 | Up -> Vector(x = 0, y = -1)
31 | Down -> Vector(x = 0, y = 1)
32 | }
33 | }
34 | }
35 |
36 | data class GameData(
37 | var score: Int = 0,
38 | var over: Boolean = false,
39 | var won: Boolean = false,
40 | )
41 |
42 | class GameManager(
43 | private val size: Int,
44 | private val storageManager: StorageManager,
45 | private val startTiles: Int = 2
46 | ) {
47 | private var grid = Grid(size)
48 | private var gameData = GameData()
49 |
50 | init {
51 | grid.addStartTiles(startTiles)
52 | }
53 |
54 | fun getGrid(): Grid = grid
55 |
56 | fun restart() {
57 | grid = Grid(size)
58 | gameData = GameData()
59 | grid.addStartTiles(startTiles)
60 | }
61 |
62 | fun move(direction: Direction) {
63 | if (isGameTerminated()) {
64 | return
65 | }
66 |
67 | val vector = direction.toVector()
68 | val traversals = buildTraversals(vector)
69 | var moved = false
70 |
71 | prepareTiles()
72 |
73 | traversals.x.forEach { x ->
74 | traversals.y.forEach { y ->
75 | val position = Position(x = x, y = y)
76 | grid.cellContent(position)?.let { tile ->
77 | val positions = findFarthestPosition(position, vector)
78 | val next = grid.cellContent(positions.next)
79 | val newPosition =
80 | if (next != null && next.value == tile.value && next.mergedFrom == null) {
81 | val merged = Tile(
82 | id = UUID.randomUUID().toString(),
83 | curPosition = positions.next,
84 | value = tile.value * 2,
85 | mergedFrom = tile to next
86 | )
87 | grid.insertTile(merged)
88 | grid.removeTile(tile)
89 | gameData.score += merged.value
90 | if (storageManager.getBestScore() < gameData.score) {
91 | storageManager.setBestScore(gameData.score)
92 | }
93 | if (merged.value == 2048) {
94 | gameData.won = true
95 | }
96 | merged.curPosition
97 | } else {
98 | moveTile(tile, positions.farthest)
99 | positions.farthest
100 | }
101 |
102 | if (position != newPosition) {
103 | moved = true
104 | }
105 | }
106 | }
107 | }
108 | if (moved) {
109 | grid.addRandomTile()
110 | if (!movesAvailable()) {
111 | gameData.over = true
112 | }
113 | }
114 | }
115 |
116 | private fun movesAvailable(): Boolean =
117 | grid.areCellsAvailable() || tileMatchesAvailable()
118 |
119 | private fun tileMatchesAvailable(): Boolean {
120 | for (x in 0 until size) {
121 | for (y in 0 until size) {
122 | val position = Position(x, y)
123 | val tile = grid.cellContent(position)
124 | if (tile != null) {
125 | for (direction in Direction.values()) {
126 | val vector = direction.toVector()
127 | val otherPosition = Position(x + vector.x, y + vector.y)
128 | val otherTile = grid.cellContent(otherPosition)
129 | if (otherTile != null && otherTile.value == tile.value) {
130 | return true
131 | }
132 | }
133 | }
134 | }
135 | }
136 | return false
137 | }
138 |
139 | private fun prepareTiles() {
140 | grid.cells
141 | .flatten()
142 | .filterNotNull()
143 | .forEach {
144 | grid.cells[it.x][it.y] = it.copy(
145 | prevPosition = it.curPosition,
146 | mergedFrom = null,
147 | )
148 | }
149 | }
150 |
151 | private fun moveTile(tile: Tile, position: Position) {
152 | grid.cells[tile.x][tile.y] = null
153 | grid.cells[position.x][position.y] = tile.copy(curPosition = position)
154 | }
155 |
156 | private fun findFarthestPosition(position: Position, vector: Vector): FarthestPosition {
157 | var curPosition: Position = position
158 | var prevPosition: Position
159 | do {
160 | prevPosition = curPosition
161 | curPosition = Position(x = prevPosition.x + vector.x, y = prevPosition.y + vector.y)
162 | } while (grid.withinBounds(curPosition) && grid.isCellAvailable(
163 | curPosition
164 | )
165 | )
166 | return FarthestPosition(
167 | farthest = prevPosition,
168 | next = curPosition,
169 | )
170 | }
171 |
172 | private fun buildTraversals(vector: Vector) =
173 | Traversals(
174 | x = if (vector.x == 1) (size - 1 downTo 0) else (0 until size),
175 | y = if (vector.y == 1) (size - 1 downTo 0) else (0 until size),
176 | )
177 |
178 | private fun Grid.addStartTiles(startTiles: Int) {
179 | repeat(startTiles) {
180 | addRandomTile()
181 | }
182 | }
183 |
184 | private fun isGameTerminated() = gameData.over
185 |
186 | private fun Grid.addRandomTile() {
187 | if (availableCells().isNotEmpty()) {
188 | val value = if (Random.nextFloat() < 0.9f) 2 else 4
189 | randomAvailableCell()?.let {
190 | insertTile(
191 | Tile(
192 | id = UUID.randomUUID().toString(),
193 | curPosition = it,
194 | value = value
195 | )
196 | )
197 | }
198 | }
199 | }
200 |
201 | fun getScore(): Int = gameData.score
202 |
203 | fun getBestScore(): Int = storageManager.getBestScore()
204 |
205 | fun getWon(): Boolean = gameData.won
206 |
207 | fun getOver(): Boolean = gameData.over
208 | }
209 |
--------------------------------------------------------------------------------
/app/src/main/java/info/degirona/compose2048/ui/game/GameScreenViewModel.kt:
--------------------------------------------------------------------------------
1 | package info.degirona.compose2048.ui.game
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.setValue
6 | import androidx.compose.ui.geometry.Offset
7 | import androidx.lifecycle.ViewModel
8 | import androidx.lifecycle.ViewModelProvider
9 | import androidx.lifecycle.viewModelScope
10 | import info.degirona.compose2048.game.Direction
11 | import info.degirona.compose2048.game.GameManager
12 | import info.degirona.compose2048.game.Grid
13 | import info.degirona.compose2048.ui.data.BoardModel
14 | import info.degirona.compose2048.ui.data.TileModel
15 | import info.degirona.compose2048.ui.data.TilePosition
16 | import kotlinx.coroutines.Dispatchers
17 | import kotlinx.coroutines.launch
18 | import java.util.UUID
19 | import kotlin.math.abs
20 |
21 | class GameScreenViewModel(
22 | private val gameManager: GameManager
23 | ) : ViewModel() {
24 | var boardModel by mutableStateOf(gameManager.toBoardModel())
25 | private set
26 |
27 | var score by mutableStateOf(0)
28 | private set
29 |
30 | var bestScore by mutableStateOf(gameManager.getBestScore())
31 | private set
32 |
33 | var won by mutableStateOf(gameManager.getWon())
34 | private set
35 |
36 | var over by mutableStateOf(gameManager.getOver())
37 | private set
38 |
39 | fun restartGame() {
40 | viewModelScope.launch(Dispatchers.Default) {
41 | gameManager.restart()
42 | score = 0
43 | boardModel = gameManager.toBoardModel()
44 | won = gameManager.getWon()
45 | over = gameManager.getOver()
46 | }
47 | }
48 |
49 | fun applyDragGesture(dragOffset: Offset) {
50 | viewModelScope.launch(Dispatchers.Default) {
51 | val direction = if (abs(dragOffset.x) > abs(dragOffset.y)) {
52 | if (dragOffset.x > 0) {
53 | Direction.Right
54 | } else {
55 | Direction.Left
56 | }
57 | } else {
58 | if (dragOffset.y > 0) {
59 | Direction.Down
60 | } else {
61 | Direction.Up
62 | }
63 | }
64 | gameManager.move(direction)
65 | boardModel = gameManager.toBoardModel()
66 | score = gameManager.getScore()
67 | bestScore = gameManager.getBestScore()
68 | won = gameManager.getWon()
69 | over = gameManager.getOver()
70 | }
71 | }
72 |
73 | private companion object {
74 | fun GameManager.toBoardModel() =
75 | BoardModel(
76 | newTiles = getGrid().toNewTiles(),
77 | staticTiles = getGrid().toStaticTiles(),
78 | swipedTiles = getGrid().toSwipedTiles(),
79 | mergedTiles = getGrid().toMergedTiles(),
80 | )
81 |
82 | fun Grid.toNewTiles() = cells
83 | .flatten()
84 | .filterNotNull()
85 | .filter { it.mergedFrom == null && it.prevPosition == null }
86 | .map {
87 | TileModel(
88 | id = it.id,
89 | curPosition = TilePosition(
90 | row = it.y.toFloat(),
91 | col = it.x.toFloat(),
92 | ),
93 | curValue = it.value,
94 | )
95 | }
96 |
97 | fun Grid.toStaticTiles() = cells
98 | .flatten()
99 | .filterNotNull()
100 | .filter { it.mergedFrom == null && it.prevPosition != null && it.curPosition == it.prevPosition }
101 | .map {
102 | TileModel(
103 | id = it.id,
104 | curPosition = TilePosition(
105 | row = it.y.toFloat(),
106 | col = it.x.toFloat(),
107 | ),
108 | curValue = it.value,
109 | )
110 | }
111 |
112 | fun Grid.toSwipedTiles() = cells
113 | .flatten()
114 | .filterNotNull()
115 | .flatMap {
116 | if (it.prevPosition != null && it.curPosition != it.prevPosition) {
117 | listOf(
118 | TileModel(
119 | id = it.id,
120 | curPosition = TilePosition(
121 | row = it.y.toFloat(),
122 | col = it.x.toFloat(),
123 | ),
124 | prevPosition = TilePosition(
125 | row = it.prevPosition.y.toFloat(),
126 | col = it.prevPosition.x.toFloat(),
127 | ),
128 | curValue = it.value,
129 | )
130 | )
131 | } else if (it.mergedFrom != null) {
132 | val prevValue = it.mergedFrom.first.value
133 | listOf(
134 | TileModel(
135 | id = UUID.randomUUID().toString(),
136 | curPosition = TilePosition(
137 | row = it.y.toFloat(),
138 | col = it.x.toFloat(),
139 | ),
140 | prevPosition = TilePosition(
141 | row = it.mergedFrom.first.curPosition.y.toFloat(),
142 | col = it.mergedFrom.first.curPosition.x.toFloat(),
143 | ),
144 | curValue = prevValue,
145 | ),
146 | TileModel(
147 | id = UUID.randomUUID().toString(),
148 | curPosition = TilePosition(
149 | row = it.y.toFloat(),
150 | col = it.x.toFloat(),
151 | ),
152 | prevPosition = TilePosition(
153 | row = it.mergedFrom.second.prevPosition!!.y.toFloat(),
154 | col = it.mergedFrom.second.prevPosition!!.x.toFloat(),
155 | ),
156 | curValue = prevValue,
157 | ),
158 | )
159 | } else emptyList()
160 | }
161 |
162 | fun Grid.toMergedTiles() = cells
163 | .flatten()
164 | .filterNotNull()
165 | .filter { it.mergedFrom != null }
166 | .map {
167 | TileModel(
168 | id = it.id,
169 | curPosition = TilePosition(
170 | row = it.y.toFloat(),
171 | col = it.x.toFloat(),
172 | ),
173 | curValue = it.value,
174 | )
175 | }
176 | }
177 | }
178 |
179 | class GameScreenViewModelFactory(
180 | private val gameManager: GameManager,
181 | ) : ViewModelProvider.NewInstanceFactory() {
182 |
183 | override fun create(modelClass: Class): T =
184 | GameScreenViewModel(gameManager) as T
185 | }
186 |
--------------------------------------------------------------------------------
/app/src/main/java/info/degirona/compose2048/ui/tile/TileRenderer.kt:
--------------------------------------------------------------------------------
1 | package info.degirona.compose2048.ui.tile
2 |
3 | import androidx.compose.animation.core.Animatable
4 | import androidx.compose.animation.core.keyframes
5 | import androidx.compose.animation.core.tween
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.LaunchedEffect
8 | import androidx.compose.runtime.key
9 | import androidx.compose.runtime.remember
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.draw.alpha
12 | import androidx.compose.ui.draw.scale
13 | import androidx.compose.ui.graphics.Color
14 | import androidx.compose.ui.unit.sp
15 | import info.degirona.compose2048.ui.board.BoardScope
16 | import info.degirona.compose2048.ui.board.Layer
17 | import info.degirona.compose2048.ui.data.TileModel
18 |
19 | private const val NEW_DURATION = 100
20 | private const val SWIPE_DURATION = 100
21 | private const val MERGE_DURATION = 200
22 |
23 | interface TileRenderer {
24 | @Composable
25 | fun BoardScope.RenderNewTile(tileModel: TileModel)
26 |
27 | @Composable
28 | fun BoardScope.RenderStaticTile(tileModel: TileModel)
29 |
30 | @Composable
31 | fun BoardScope.RenderSwipedTile(tileModel: TileModel)
32 |
33 | @Composable
34 | fun BoardScope.RenderMergedTile(tileModel: TileModel)
35 | }
36 |
37 | object TileRendererInstance : TileRenderer {
38 |
39 | @Composable
40 | override fun BoardScope.RenderNewTile(tileModel: TileModel) {
41 | key(tileModel.id) {
42 | val scale = remember { Animatable(0f) }
43 | val alpha = remember { Animatable(0f) }
44 | Tile(
45 | fraction = tileFraction,
46 | value = tileModel.curValue.toString(),
47 | color = tileModel.curValue.toTextColor(),
48 | fontSize = tileModel.curValue.toFontSize(),
49 | backgroundColor = tileModel.curValue.toBackgroundColor(),
50 | modifier = Modifier
51 | .boardCell(
52 | row = tileModel.curPosition.row,
53 | col = tileModel.curPosition.col,
54 | layer = Layer.AnimationLayer
55 | )
56 | .scale(scale.value)
57 | .alpha(alpha.value)
58 | )
59 | LaunchedEffect(this) {
60 | scale.animateTo(
61 | targetValue = 1f,
62 | animationSpec = tween(
63 | delayMillis = SWIPE_DURATION,
64 | durationMillis = NEW_DURATION
65 | )
66 | )
67 | }
68 | LaunchedEffect(this) {
69 | alpha.animateTo(
70 | targetValue = 1f,
71 | animationSpec = tween(
72 | delayMillis = SWIPE_DURATION,
73 | durationMillis = NEW_DURATION
74 | )
75 | )
76 | }
77 | }
78 | }
79 |
80 | @Composable
81 | override fun BoardScope.RenderStaticTile(tileModel: TileModel) {
82 | key(tileModel.id) {
83 | Tile(
84 | fraction = tileFraction,
85 | value = tileModel.curValue.toString(),
86 | color = tileModel.curValue.toTextColor(),
87 | fontSize = tileModel.curValue.toFontSize(),
88 | backgroundColor = tileModel.curValue.toBackgroundColor(),
89 | modifier = Modifier
90 | .boardCell(
91 | row = tileModel.curPosition.row,
92 | col = tileModel.curPosition.col,
93 | layer = Layer.CellLayer
94 | )
95 | )
96 | }
97 | }
98 |
99 | @Composable
100 | override fun BoardScope.RenderSwipedTile(tileModel: TileModel) {
101 | key(tileModel.id) {
102 | val row = remember { Animatable(tileModel.prevPosition!!.row) }
103 | val col = remember { Animatable(tileModel.prevPosition!!.col) }
104 | Tile(
105 | fraction = tileFraction,
106 | value = tileModel.curValue.toString(),
107 | color = tileModel.curValue.toTextColor(),
108 | fontSize = tileModel.curValue.toFontSize(),
109 | backgroundColor = tileModel.curValue.toBackgroundColor(),
110 | modifier = Modifier
111 | .boardCell(
112 | row = row.value,
113 | col = col.value,
114 | layer = Layer.AnimationLayer
115 | )
116 | )
117 | if (row.value != tileModel.curPosition.row) {
118 | LaunchedEffect(this) {
119 | row.animateTo(
120 | targetValue = tileModel.curPosition.row,
121 | animationSpec = tween(durationMillis = SWIPE_DURATION)
122 | )
123 | }
124 | }
125 | if (col.value != tileModel.curPosition.col) {
126 | LaunchedEffect(this) {
127 | col.animateTo(
128 | targetValue = tileModel.curPosition.col,
129 | animationSpec = tween(durationMillis = SWIPE_DURATION)
130 | )
131 | }
132 | }
133 | }
134 | }
135 |
136 | @Composable
137 | override fun BoardScope.RenderMergedTile(tileModel: TileModel) {
138 | key(tileModel.id) {
139 | val scale = remember { Animatable(0f) }
140 | Tile(
141 | fraction = tileFraction,
142 | value = tileModel.curValue.toString(),
143 | color = tileModel.curValue.toTextColor(),
144 | fontSize = tileModel.curValue.toFontSize(),
145 | backgroundColor = tileModel.curValue.toBackgroundColor(),
146 | modifier = Modifier
147 | .boardCell(
148 | row = tileModel.curPosition.row,
149 | col = tileModel.curPosition.col,
150 | layer = Layer.MergeLayer
151 | )
152 | .scale(scale.value),
153 | )
154 | LaunchedEffect(this) {
155 | scale.animateTo(
156 | targetValue = 1f,
157 | animationSpec = keyframes {
158 | delayMillis = SWIPE_DURATION
159 | durationMillis = MERGE_DURATION
160 | 0f atFraction 0f
161 | 1.2f atFraction 0.5f
162 | 1f atFraction 1f
163 | }
164 | )
165 | }
166 | }
167 | }
168 |
169 | private fun Int.toBackgroundColor() =
170 | when {
171 | this == 2 -> Color(0xffeee4da)
172 | this == 4 -> Color(0xffede0c8)
173 | this == 8 -> Color(0xfff2b179)
174 | this == 16 -> Color(0xfff59563)
175 | this == 32 -> Color(0xfff67c5f)
176 | this == 64 -> Color(0xfff65e3b)
177 | this == 128 -> Color(0xffedcf72)
178 | this == 256 -> Color(0xffedcc61)
179 | this == 512 -> Color(0xffedc850)
180 | this == 1024 -> Color(0xffedc53f)
181 | this == 2048 -> Color(0xffedc22e)
182 | else -> Color(0xff3c3a32)
183 | }
184 |
185 | private fun Int.toTextColor() =
186 | if (this < 8) Color(0xff776e65) else Color(0xfff8f6f2)
187 |
188 | private fun Int.toFontSize() =
189 | when {
190 | this == 2 -> 40.sp
191 | this == 4 -> 40.sp
192 | this == 8 -> 40.sp
193 | this == 16 -> 40.sp
194 | this == 32 -> 40.sp
195 | this == 64 -> 40.sp
196 | this == 128 -> 32.sp
197 | this == 256 -> 32.sp
198 | this == 512 -> 32.sp
199 | this == 1024 -> 24.sp
200 | this == 2048 -> 24.sp
201 | else -> 20.sp
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/app/config/detekt.yml:
--------------------------------------------------------------------------------
1 | build:
2 | maxIssues: 0
3 | weights:
4 | complexity: 2
5 | LongParameterList: 1
6 | comments: 1
7 |
8 | processors:
9 | active: false
10 |
11 | console-reports:
12 | active: true
13 |
14 | output-reports:
15 | active: true
16 | exclude:
17 | - 'HtmlOutputReport'
18 | - 'TxtOutputReport'
19 |
20 | potential-bugs:
21 | active: true
22 | DuplicateCaseInWhenExpression:
23 | active: true
24 | EqualsAlwaysReturnsTrueOrFalse:
25 | active: false
26 | EqualsWithHashCodeExist:
27 | active: true
28 | WrongEqualsTypeParameter:
29 | active: false
30 | IteratorHasNextCallsNextMethod:
31 | active: false
32 | ExplicitGarbageCollectionCall:
33 | active: true
34 | UnconditionalJumpStatementInLoop:
35 | active: false
36 | IteratorNotThrowingNoSuchElementException:
37 | active: false
38 | UnreachableCode:
39 | active: true
40 | LateinitUsage:
41 | active: false
42 | ignoreAnnotated: []
43 | ignoreOnClassesPattern: ''
44 | UnsafeCallOnNullableType:
45 | active: false
46 | UnsafeCast:
47 | active: false
48 | UselessPostfixExpression:
49 | active: false
50 |
51 | performance:
52 | active: true
53 | ForEachOnRange:
54 | active: true
55 | SpreadOperator:
56 | active: false
57 | UnnecessaryTemporaryInstantiation:
58 | active: true
59 |
60 | exceptions:
61 | active: true
62 | ExceptionRaisedInUnexpectedLocation:
63 | active: false
64 | methodNames: ['toString', 'hashCode', 'equals', 'finalize']
65 | SwallowedException:
66 | active: false
67 | TooGenericExceptionCaught:
68 | active: true
69 | excludes: [ '**/src/test/**', '**/src/androidTest/**', '**/*Test.kt', '**/*.Spec.kt' ]
70 | exceptionNames:
71 | - ArrayIndexOutOfBoundsException
72 | - Error
73 | - Exception
74 | - IllegalMonitorStateException
75 | - IndexOutOfBoundsException
76 | - InterruptedException
77 | - NullPointerException
78 | - RuntimeException
79 | - Throwable
80 | allowedExceptionNameRegex: 'e|_|(ignore|expected).*'
81 | TooGenericExceptionThrown:
82 | active: true
83 | exceptionNames:
84 | - Error
85 | - Exception
86 | - NullPointerException
87 | - RuntimeException
88 | - Throwable
89 | InstanceOfCheckForException:
90 | active: false
91 | NotImplementedDeclaration:
92 | active: true
93 | ThrowingExceptionsWithoutMessageOrCause:
94 | active: true
95 | exceptions: ['IllegalArgumentException', 'IllegalStateException', 'IOException']
96 | PrintStackTrace:
97 | active: false
98 | RethrowCaughtException:
99 | active: false
100 | ReturnFromFinally:
101 | active: true
102 | ThrowingExceptionFromFinally:
103 | active: true
104 | ThrowingExceptionInMain:
105 | active: false
106 | ThrowingNewInstanceOfSameException:
107 | active: false
108 |
109 | empty-blocks:
110 | active: true
111 | EmptyCatchBlock:
112 | active: true
113 | EmptyClassBlock:
114 | active: false
115 | EmptyDefaultConstructor:
116 | active: true
117 | EmptyDoWhileBlock:
118 | active: true
119 | EmptyElseBlock:
120 | active: true
121 | EmptyFinallyBlock:
122 | active: true
123 | EmptyForBlock:
124 | active: true
125 | EmptyFunctionBlock:
126 | active: true
127 | ignoreOverridden: true
128 | EmptyIfBlock:
129 | active: true
130 | EmptyInitBlock:
131 | active: true
132 | EmptyKtFile:
133 | active: true
134 | EmptySecondaryConstructor:
135 | active: true
136 | EmptyWhenBlock:
137 | active: true
138 | EmptyWhileBlock:
139 | active: true
140 |
141 | complexity:
142 | active: true
143 | LongMethod:
144 | active: true
145 | excludes: [ '**/src/test/**', '**/src/androidTest/**', '**/*Test.kt', '**/*.Spec.kt' ]
146 | threshold: 60
147 | NestedBlockDepth:
148 | active: true
149 | excludes: [ '**/src/test/**', '**/src/androidTest/**', '**/*Test.kt', '**/*.Spec.kt' ]
150 | threshold: 4 # TODO: create custom rule that doesn't count run/let/apply/etc.
151 | LongParameterList:
152 | active: true
153 | excludes: [ '**/src/test/**', '**/src/androidTest/**', '**/*Test.kt', '**/*.Spec.kt', '**/*Builder.kt', '**/*Module.kt', '**/designdemo/**/*Entity.kt' ]
154 | functionThreshold: 6
155 | constructorThreshold: 100
156 | ignoreDefaultParameters: true
157 | ignoreDataClasses: true
158 | ignoreAnnotated: [ 'Inject', 'Composable' ]
159 | LargeClass:
160 | active: true
161 | excludes: [ '**/src/test/**', '**/src/androidTest/**', '**/*Test.kt', '**/*.Spec.kt' ]
162 | threshold: 600
163 | ComplexInterface:
164 | active: false
165 | threshold: 10
166 | includeStaticDeclarations: false
167 | ComplexMethod:
168 | active: false
169 | threshold: 10
170 | MethodOverloading:
171 | active: false
172 | threshold: 5
173 | TooManyFunctions:
174 | active: false
175 | thresholdInFiles: 10
176 | thresholdInClasses: 10
177 | thresholdInInterfaces: 10
178 | thresholdInObjects: 10
179 | thresholdInEnums: 10
180 | ComplexCondition:
181 | active: true
182 | threshold: 5
183 | LabeledExpression:
184 | active: false
185 | StringLiteralDuplication:
186 | active: false
187 | threshold: 2
188 | ignoreAnnotation: true
189 | excludeStringsWithLessThan5Characters: true
190 | ignoreStringsRegex: '$^'
191 |
192 | naming:
193 | active: true
194 | MemberNameEqualsClassName:
195 | active: false
196 | ignoreOverridden: true
197 | ClassNaming:
198 | active: true
199 | excludes: [ '**/src/test/**', '**/src/androidTest/**', '**/*Test.kt', '**/*.Spec.kt' ]
200 | classPattern: '[A-Z][a-zA-Z0-9]*'
201 | ConstructorParameterNaming:
202 | active: true
203 | excludes: [ '**/src/test/**', '**/src/androidTest/**', '**/*Test.kt', '**/*.Spec.kt' ]
204 | parameterPattern: '[a-z][A-Za-z0-9]*'
205 | privateParameterPattern: '[a-z][A-Za-z0-9]*'
206 | excludeClassPattern: '$^'
207 | ignoreOverridden: true
208 | EnumNaming:
209 | active: true
210 | enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
211 | PackageNaming:
212 | active: false
213 | FunctionNaming:
214 | active: true
215 | excludes: [ '**/src/test/**', '**/src/androidTest/**', '**/*Test.kt', '**/*.Spec.kt' ]
216 | functionPattern: '([a-z][a-zA-Z0-9]*)|(`.*`)'
217 | ignoreAnnotated: ['Composable']
218 | FunctionParameterNaming:
219 | active: true
220 | excludes: [ '**/src/test/**', '**/src/androidTest/**', '**/*Test.kt', '**/*.Spec.kt' ]
221 | parameterPattern: '[a-z][A-Za-z0-9]*'
222 | excludeClassPattern: '$^'
223 | ignoreOverridden: true
224 | FunctionMaxLength:
225 | active: false
226 | maximumFunctionNameLength: 30
227 | FunctionMinLength:
228 | active: false
229 | minimumFunctionNameLength: 3
230 | VariableNaming:
231 | active: true
232 | excludes: [ '**/src/test/**', '**/src/androidTest/**', '**/*Test.kt', '**/*.Spec.kt' ]
233 | variablePattern: '[a-z][A-Za-z0-9]*'
234 | privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
235 | excludeClassPattern: '$^'
236 | TopLevelPropertyNaming:
237 | active: true
238 | excludes: [ '**/src/test/**', '**/src/androidTest/**', '**/*Test.kt', '**/*.Spec.kt' ]
239 | constantPattern: '[A-Z][_A-Z0-9]*'
240 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
241 | privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
242 | VariableMaxLength:
243 | active: false
244 | maximumVariableNameLength: 30
245 | VariableMinLength:
246 | active: false
247 | minimumVariableNameLength: 3
248 | ForbiddenClassName:
249 | active: false
250 | forbiddenName: []
251 |
252 | style:
253 | active: true
254 | ReturnCount:
255 | active: true
256 | max: 3
257 | ThrowsCount:
258 | active: true
259 | max: 2
260 | NewLineAtEndOfFile:
261 | active: true
262 | OptionalAbstractKeyword:
263 | active: true
264 | OptionalWhenBraces:
265 | active: false
266 | CollapsibleIfStatements:
267 | active: false
268 | EqualsNullCall:
269 | active: false
270 | EqualsOnSignatureLine:
271 | active: true
272 | ForbiddenComment:
273 | active: true
274 | values: ['STOPSHIP']
275 | ForbiddenImport:
276 | active: true
277 | imports: ['kotlinx.android.parcel.Parcelize']
278 | LoopWithTooManyJumpStatements:
279 | active: true
280 | maxJumpCount: 1
281 | ModifierOrder:
282 | active: true
283 | MagicNumber:
284 | active: true
285 | excludes: [ '**/src/test/**', '**/src/androidTest/**', '**/*Test.kt', '**/*.Spec.kt', '**/designdemo/**/*Entity.kt' ]
286 | ignoreNumbers: ['-1','0','1','2']
287 | ignoreHashCodeFunction: false
288 | ignorePropertyDeclaration: false
289 | ignoreConstantDeclaration: true
290 | ignoreCompanionObjectPropertyDeclaration: true
291 | ignoreAnnotation: false
292 | ignoreNamedArgument: true
293 | ignoreEnums: false
294 | WildcardImport:
295 | excludes: [ '**/src/test/**', '**/src/androidTest/**', '**/*Test.kt', '**/*.Spec.kt' ]
296 | active: true
297 | SafeCast:
298 | active: true
299 | MaxLineLength:
300 | active: true
301 | excludes: [ '**/src/test/**', '**/src/androidTest/**', '**/*Test.kt', '**/*.Spec.kt' ]
302 | maxLineLength: 160
303 | excludePackageStatements: false
304 | excludeImportStatements: false
305 | ProtectedMemberInFinalClass:
306 | active: false
307 | SerialVersionUIDInSerializableClass:
308 | active: false
309 | UnnecessaryParentheses:
310 | active: false
311 | UnnecessaryInheritance:
312 | active: false
313 | UtilityClassWithPublicConstructor:
314 | active: false
315 | DataClassContainsFunctions:
316 | active: false
317 | conversionFunctionPrefix: 'to'
318 | UseDataClass:
319 | active: false
320 | UnnecessaryAbstractClass:
321 | active: false
322 | OptionalUnit:
323 | active: true
324 | ExpressionBodySyntax:
325 | active: true
326 | UnusedImports:
327 | active: true
328 | NestedClassesVisibility:
329 | active: true
330 | RedundantVisibilityModifierRule:
331 | active: true
332 | FunctionOnlyReturningConstant:
333 | active: true
334 | ignoreAnnotated: ['Provides']
335 | ignoreOverridableFunction: true
336 | excludedFunctions: 'describeContents'
337 | LibraryCodeMustSpecifyReturnType:
338 | active: true
339 | excludes: [ '**/src/test/**', '**/src/androidTest/**', '**/*Test.kt', '**/*.Spec.kt', '**/*InstrumentationTestUtils/**' ]
340 | MandatoryBracesIfStatements:
341 | active: true
342 | MayBeConst:
343 | active: true
344 |
345 | comments:
346 | active: true
347 | CommentOverPrivateFunction:
348 | active: false
349 | CommentOverPrivateProperty:
350 | excludes: [ '**/src/test/**', '**/src/androidTest/**', '**/*Test.kt', '**/*.Spec.kt', '**/*InstrumentationTestUtils/**' ]
351 | active: true
352 | UndocumentedPublicClass:
353 | active: false
354 | searchInNestedClass: true
355 | searchInInnerClass: true
356 | searchInInnerObject: true
357 | searchInInnerInterface: true
358 | UndocumentedPublicFunction:
359 | active: false
360 |
--------------------------------------------------------------------------------