├── 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 | ![Sample](compose-2048.gif) 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 | --------------------------------------------------------------------------------