├── .idea ├── .name ├── .gitignore ├── compiler.xml ├── kotlinc.xml ├── vcs.xml ├── migrations.xml ├── deploymentTargetSelector.xml ├── gradle.xml ├── appInsightsSettings.xml ├── misc.xml └── inspectionProfiles │ └── Project_Default.xml ├── pictures ├── game2048-anim.gif └── game2048-screenshot.jpg ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── composeApp ├── src │ ├── commonMain │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── joaomanaia │ │ │ │ └── game2048 │ │ │ │ ├── model │ │ │ │ ├── Grid.kt │ │ │ │ ├── Cell.kt │ │ │ │ ├── Direction.kt │ │ │ │ ├── GridTile.kt │ │ │ │ ├── Tile.kt │ │ │ │ ├── GridTileMovement.kt │ │ │ │ └── HslColor.kt │ │ │ │ ├── core │ │ │ │ ├── util │ │ │ │ │ ├── StringUtil.kt │ │ │ │ │ └── ListUtil.kt │ │ │ │ ├── common │ │ │ │ │ ├── GameCommon.kt │ │ │ │ │ └── preferences │ │ │ │ │ │ └── GameDataPreferencesCommon.kt │ │ │ │ ├── navigation │ │ │ │ │ └── Screen.kt │ │ │ │ ├── datastore │ │ │ │ │ ├── manager │ │ │ │ │ │ ├── DataStoreManagerImpl.kt │ │ │ │ │ │ ├── PreferenceRequest.kt │ │ │ │ │ │ └── DataStoreManager.kt │ │ │ │ │ └── PreferencesKey.kt │ │ │ │ ├── presentation │ │ │ │ │ └── theme │ │ │ │ │ │ ├── Type.kt │ │ │ │ │ │ ├── WallpaperColors.kt │ │ │ │ │ │ ├── DarkThemeConfig.kt │ │ │ │ │ │ ├── Spacing.kt │ │ │ │ │ │ ├── TileColorsGenerator.kt │ │ │ │ │ │ ├── Color.kt │ │ │ │ │ │ └── Theme.kt │ │ │ │ └── compose │ │ │ │ │ └── KeyEventHandler.kt │ │ │ │ ├── di │ │ │ │ ├── DataStoreModule.kt │ │ │ │ ├── WallpaperColorsModule.kt │ │ │ │ ├── AppModule.kt │ │ │ │ ├── KoinStarter.kt │ │ │ │ └── RepositoryModule.kt │ │ │ │ ├── presentation │ │ │ │ ├── game │ │ │ │ │ ├── GameScreenUiEvent.kt │ │ │ │ │ ├── GameScreenUiState.kt │ │ │ │ │ └── components │ │ │ │ │ │ ├── grid │ │ │ │ │ │ ├── GridContainer.kt │ │ │ │ │ │ ├── GameGrid.kt │ │ │ │ │ │ ├── ChangeGameGridDialog.kt │ │ │ │ │ │ └── GridTileText.kt │ │ │ │ │ │ └── icons │ │ │ │ │ │ └── Grid4x4.kt │ │ │ │ ├── MainUiState.kt │ │ │ │ ├── color_settings │ │ │ │ │ ├── ColorSettingsUiState.kt │ │ │ │ │ ├── ColorSettingsUiEvent.kt │ │ │ │ │ ├── components │ │ │ │ │ │ ├── SelectablePaletteItem.kt │ │ │ │ │ │ ├── BaseColorChooser.kt │ │ │ │ │ │ └── DarkThemeDialogPicker.kt │ │ │ │ │ └── ColorSettingsScreenViewModel.kt │ │ │ │ ├── components │ │ │ │ │ ├── BackIconButton.kt │ │ │ │ │ └── GameDialog.kt │ │ │ │ ├── MainViewModel.kt │ │ │ │ └── App.kt │ │ │ │ ├── domain │ │ │ │ ├── repository │ │ │ │ │ └── SaveGameRepository.kt │ │ │ │ └── usecase │ │ │ │ │ └── GetHueParamsUseCase.kt │ │ │ │ └── data │ │ │ │ └── repository │ │ │ │ └── SaveGameRepositoryImpl.kt │ │ └── composeResources │ │ │ ├── values │ │ │ └── strings.xml │ │ │ ├── values-pt │ │ │ └── strings.xml │ │ │ ├── values-es │ │ │ └── strings.xml │ │ │ └── values-fr │ │ │ └── strings.xml │ ├── androidMain │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_launcher_foreground.png │ │ │ ├── values │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── themes.xml │ │ │ │ ├── strings.xml │ │ │ │ └── colors.xml │ │ │ ├── values-night │ │ │ │ └── themes.xml │ │ │ ├── values-pt │ │ │ │ └── strings.xml │ │ │ ├── values-es │ │ │ │ └── strings.xml │ │ │ ├── values-fr │ │ │ │ └── strings.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ └── drawable │ │ │ │ └── ic_launcher_background.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── joaomanaia │ │ │ │ └── game2048 │ │ │ │ ├── core │ │ │ │ ├── compose │ │ │ │ │ └── preview │ │ │ │ │ │ └── BooleanPreviewProvider.kt │ │ │ │ └── presentation │ │ │ │ │ └── theme │ │ │ │ │ ├── Theme.android.kt │ │ │ │ │ └── WallpaperColors.android.kt │ │ │ │ ├── di │ │ │ │ ├── WallpaperColorsModule.android.kt │ │ │ │ └── DataStoreModule.android.kt │ │ │ │ ├── Game2048App.kt │ │ │ │ ├── presentation │ │ │ │ ├── color_settings │ │ │ │ │ ├── ColorSettingsScreenPreview.kt │ │ │ │ │ └── components │ │ │ │ │ │ ├── DarkThemeDialogPickerPreview.kt │ │ │ │ │ │ ├── PaletteItemPreview.kt │ │ │ │ │ │ └── BaseColorChooserPreview.kt │ │ │ │ └── game │ │ │ │ │ └── components │ │ │ │ │ └── grid │ │ │ │ │ └── GridTitleTextPreview.kt │ │ │ │ └── MainActivity.kt │ │ └── AndroidManifest.xml │ ├── androidJvmMain │ │ └── kotlin │ │ │ └── com │ │ │ └── joaomanaia │ │ │ └── game2048 │ │ │ └── core │ │ │ ├── util │ │ │ └── StringUtil.androidJvm.kt │ │ │ └── datastore │ │ │ ├── PreferencesKey.androidJvm.kt │ │ │ └── manager │ │ │ └── DataStoreManagerImpl.androidJvm.kt │ ├── desktopMain │ │ └── kotlin │ │ │ ├── com │ │ │ └── joaomanaia │ │ │ │ └── game2048 │ │ │ │ ├── di │ │ │ │ ├── WallpaperColorsModule.desktop.kt │ │ │ │ └── DataStoreModule.desktop.kt │ │ │ │ └── core │ │ │ │ └── presentation │ │ │ │ └── theme │ │ │ │ ├── WallpaperColors.desktop.kt │ │ │ │ └── Theme.desktop.kt │ │ │ └── main.kt │ ├── wasmJsMain │ │ ├── kotlin │ │ │ ├── com │ │ │ │ └── joaomanaia │ │ │ │ │ └── game2048 │ │ │ │ │ ├── di │ │ │ │ │ ├── WallpaperColorsModule.wasmJs.kt │ │ │ │ │ └── DataStoreModule.wasmJs.kt │ │ │ │ │ └── core │ │ │ │ │ ├── presentation │ │ │ │ │ └── theme │ │ │ │ │ │ ├── WallpaperColors.wasmJs.kt │ │ │ │ │ │ └── Theme.wasmJs.kt │ │ │ │ │ ├── util │ │ │ │ │ └── StringUtil.wasmJs.kt │ │ │ │ │ └── datastore │ │ │ │ │ ├── manager │ │ │ │ │ └── DataStoreManagerImpl.wasmJs.kt │ │ │ │ │ └── PreferencesKey.wasmJs.kt │ │ │ └── main.kt │ │ └── resources │ │ │ └── index.html │ └── commonTest │ │ └── kotlin │ │ └── com │ │ └── joaomanaia │ │ └── game2048 │ │ ├── model │ │ ├── TileTest.kt │ │ └── HslColorTest.kt │ │ └── core │ │ └── util │ │ ├── ListRotationTest.kt │ │ ├── GameUtilTest.kt │ │ └── GridMovementTest.kt ├── build │ └── generated │ │ └── compose │ │ └── resourceGenerator │ │ └── kotlin │ │ └── commonResClass │ │ └── game2048 │ │ └── composeapp │ │ └── generated │ │ └── resources │ │ └── Res.kt ├── compose-desktop.pro └── build.gradle.kts ├── settings.gradle.kts ├── .gitignore ├── README.md ├── .github └── workflows │ └── build.yml ├── gradle.properties ├── gradlew.bat └── gradlew /.idea/.name: -------------------------------------------------------------------------------- 1 | Game2048 -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /pictures/game2048-anim.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaomanaia/game-2048-compose/HEAD/pictures/game2048-anim.gif -------------------------------------------------------------------------------- /pictures/game2048-screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaomanaia/game-2048-compose/HEAD/pictures/game2048-screenshot.jpg -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaomanaia/game-2048-compose/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/model/Grid.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.model 2 | 3 | typealias Grid = List> 4 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaomanaia/game-2048-compose/HEAD/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaomanaia/game-2048-compose/HEAD/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaomanaia/game-2048-compose/HEAD/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaomanaia/game-2048-compose/HEAD/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaomanaia/game-2048-compose/HEAD/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaomanaia/game-2048-compose/HEAD/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaomanaia/game-2048-compose/HEAD/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaomanaia/game-2048-compose/HEAD/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaomanaia/game-2048-compose/HEAD/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaomanaia/game-2048-compose/HEAD/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #EBC33F 4 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/core/util/StringUtil.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.util 2 | 3 | expect fun formatSettingTrailingNumber(value: Float): String 4 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaomanaia/game-2048-compose/HEAD/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaomanaia/game-2048-compose/HEAD/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/core/common/GameCommon.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.common 2 | 3 | object GameCommon { 4 | const val NUM_INITIAL_TILES = 2 5 | } -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaomanaia/game-2048-compose/HEAD/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaomanaia/game-2048-compose/HEAD/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaomanaia/game-2048-compose/HEAD/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/core/navigation/Screen.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.navigation 2 | 3 | enum class Screen { 4 | GAME, 5 | COLOR_SETTINGS 6 | } 7 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/di/DataStoreModule.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.di 2 | 3 | import org.koin.core.module.Module 4 | 5 | expect val dataStoreModule: Module 6 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/di/WallpaperColorsModule.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.di 2 | 3 | import org.koin.core.module.Module 4 | 5 | expect val wallpaperColorsModule: Module 6 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/core/datastore/manager/DataStoreManagerImpl.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.datastore.manager 2 | 3 | expect class DataStoreManagerImpl : DataStoreManager 4 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/core/presentation/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.presentation.theme 2 | 3 | import androidx.compose.material3.Typography 4 | 5 | val AppTypography = Typography() 6 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/model/Cell.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.model 2 | 3 | /** 4 | * Container class that describes a location in a 2D grid. 5 | */ 6 | data class Cell( 7 | val row: Int, 8 | val col: Int 9 | ) 10 | -------------------------------------------------------------------------------- /composeApp/src/androidJvmMain/kotlin/com/joaomanaia/game2048/core/util/StringUtil.androidJvm.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.util 2 | 3 | import kotlin.text.format 4 | 5 | actual fun formatSettingTrailingNumber(value: Float): String { 6 | return "%.2f".format(value) 7 | } 8 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/model/Direction.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.model 2 | 3 | /** 4 | * Enum describing the 4 different swipe directions. 5 | */ 6 | enum class Direction { 7 | UP, 8 | DOWN, 9 | LEFT, 10 | RIGHT 11 | } 12 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Mar 11 15:18:46 WET 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/model/GridTile.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.model 2 | 3 | /** 4 | * Container class describing a tile at a certain location in the grid. 5 | */ 6 | data class GridTile( 7 | val cell: Cell, 8 | val tile: Tile 9 | ) 10 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/core/datastore/manager/PreferenceRequest.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.datastore.manager 2 | 3 | import com.joaomanaia.game2048.core.datastore.PreferencesKey 4 | 5 | open class PreferenceRequest( 6 | val key: PreferencesKey, 7 | val defaultValue: T 8 | ) 9 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/joaomanaia/game2048/core/compose/preview/BooleanPreviewProvider.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.compose.preview 2 | 3 | import androidx.compose.ui.tooling.preview.PreviewParameterProvider 4 | 5 | class BooleanPreviewProvider : PreviewParameterProvider { 6 | override val values: Sequence = sequenceOf(true, false) 7 | } 8 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/joaomanaia/game2048/di/WallpaperColorsModule.android.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.di 2 | 3 | import org.koin.core.module.dsl.singleOf 4 | import org.koin.dsl.module 5 | import com.joaomanaia.game2048.core.presentation.theme.WallpaperColors 6 | 7 | actual val wallpaperColorsModule = module { 8 | singleOf(::WallpaperColors) 9 | } 10 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/com/joaomanaia/game2048/di/WallpaperColorsModule.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.di 2 | 3 | import org.koin.core.module.dsl.singleOf 4 | import org.koin.dsl.module 5 | import com.joaomanaia.game2048.core.presentation.theme.WallpaperColors 6 | 7 | actual val wallpaperColorsModule = module { 8 | singleOf(::WallpaperColors) 9 | } 10 | -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/kotlin/com/joaomanaia/game2048/di/WallpaperColorsModule.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.di 2 | 3 | import com.joaomanaia.game2048.core.presentation.theme.WallpaperColors 4 | import org.koin.core.module.dsl.singleOf 5 | import org.koin.dsl.module 6 | 7 | actual val wallpaperColorsModule = module { 8 | singleOf(::WallpaperColors) 9 | } 10 | -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/kotlin/com/joaomanaia/game2048/core/presentation/theme/WallpaperColors.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.presentation.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | actual class WallpaperColors { 6 | actual fun isSupported(): Boolean = false 7 | 8 | actual fun generateWallpaperColors(): Set = emptySet() 9 | } 10 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/com/joaomanaia/game2048/core/presentation/theme/WallpaperColors.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.presentation.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | actual class WallpaperColors { 6 | actual fun isSupported(): Boolean = false 7 | 8 | actual fun generateWallpaperColors(): Set = emptySet() 9 | } 10 | -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/kotlin/com/joaomanaia/game2048/core/util/StringUtil.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.util 2 | 3 | actual fun formatSettingTrailingNumber(value: Float): String { 4 | val integerPart = value.toInt() 5 | val decimalPart = ((value - integerPart) * 100).toInt() 6 | 7 | return if (decimalPart < 10) { 8 | "$integerPart.0$decimalPart" 9 | } else { 10 | "$integerPart.$decimalPart" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Game 2048 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/presentation/game/GameScreenUiEvent.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.presentation.game 2 | 3 | import com.joaomanaia.game2048.model.Direction 4 | 5 | sealed interface GameScreenUiEvent { 6 | data object OnStartNewGameRequest : GameScreenUiEvent 7 | 8 | data class OnMoveGrid(val direction: Direction) : GameScreenUiEvent 9 | 10 | data class OnGridSizeChange(val newSize: Int) : GameScreenUiEvent 11 | } 12 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/kotlin/com/joaomanaia/game2048/di/DataStoreModule.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.di 2 | 3 | import com.joaomanaia.game2048.core.datastore.manager.DataStoreManager 4 | import com.joaomanaia.game2048.core.datastore.manager.DataStoreManagerImpl 5 | import org.koin.core.module.dsl.singleOf 6 | import org.koin.dsl.bind 7 | import org.koin.dsl.module 8 | 9 | actual val dataStoreModule = module { 10 | singleOf(::DataStoreManagerImpl) bind DataStoreManager::class 11 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 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 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev/") 14 | } 15 | } 16 | 17 | rootProject.name = "Game2048" 18 | include(":composeApp") 19 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/core/presentation/theme/WallpaperColors.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.presentation.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | expect class WallpaperColors { 6 | fun isSupported(): Boolean 7 | 8 | fun generateWallpaperColors(): Set 9 | } 10 | 11 | object WallpaperColorsDefaults { 12 | const val DEFAULT_HUE_SHIFT = 10 13 | const val DEFAULT_SATURATION_SHIFT = 0.1f 14 | const val DEFAULT_LIGHTNESS_SHIFT = 0.1f 15 | } 16 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/presentation/MainUiState.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.presentation 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import com.joaomanaia.game2048.core.presentation.theme.DarkThemeConfig 5 | 6 | sealed interface MainUiState { 7 | data object Loading : MainUiState 8 | 9 | data class Success( 10 | val darkThemeConfig: DarkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM, 11 | val amoledMode: Boolean = false, 12 | val seedColor: Color? = null 13 | ) : MainUiState 14 | } 15 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.di 2 | 3 | import com.joaomanaia.game2048.presentation.MainViewModel 4 | import com.joaomanaia.game2048.presentation.color_settings.ColorSettingsScreenViewModel 5 | import com.joaomanaia.game2048.presentation.game.GameViewModel 6 | import org.koin.compose.viewmodel.dsl.viewModelOf 7 | import org.koin.dsl.module 8 | 9 | val appModule = module { 10 | viewModelOf(::MainViewModel) 11 | viewModelOf(::GameViewModel) 12 | viewModelOf(::ColorSettingsScreenViewModel) 13 | } 14 | -------------------------------------------------------------------------------- /composeApp/src/androidJvmMain/kotlin/com/joaomanaia/game2048/core/datastore/PreferencesKey.androidJvm.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.datastore 2 | 3 | import androidx.datastore.preferences.core.Preferences as AndroidxPreferences 4 | import androidx.datastore.preferences.core.MutablePreferences as AndroidxMutablePreferences 5 | 6 | actual typealias Preferences = AndroidxPreferences 7 | actual typealias MutablePreferences = AndroidxMutablePreferences 8 | 9 | actual typealias PreferencesKey = AndroidxPreferences.Key 10 | actual typealias PreferencesPair = AndroidxPreferences.Pair 11 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/core/presentation/theme/DarkThemeConfig.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.presentation.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.runtime.Composable 5 | 6 | enum class DarkThemeConfig { 7 | FOLLOW_SYSTEM, 8 | DARK, 9 | LIGHT; 10 | 11 | @Composable 12 | fun shouldUseDarkTheme(): Boolean { 13 | return when (this) { 14 | FOLLOW_SYSTEM -> isSystemInDarkTheme() 15 | DARK -> true 16 | LIGHT -> false 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/di/KoinStarter.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.di 2 | 3 | import io.github.oshai.kotlinlogging.KotlinLogging 4 | import org.koin.core.context.startKoin 5 | import org.koin.dsl.KoinAppDeclaration 6 | 7 | object KoinStarter { 8 | private val logger = KotlinLogging.logger("KoinStarter") 9 | 10 | fun init(config: KoinAppDeclaration? = null) = startKoin { 11 | logger.trace { "Starting Koin" } 12 | config?.invoke(this) 13 | modules(appModule, dataStoreModule, repositoryModule, wallpaperColorsModule) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/di/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.di 2 | 3 | import com.joaomanaia.game2048.data.repository.SaveGameRepositoryImpl 4 | import com.joaomanaia.game2048.domain.repository.SaveGameRepository 5 | import com.joaomanaia.game2048.domain.usecase.GetHueParamsUseCase 6 | import org.koin.core.module.dsl.singleOf 7 | import org.koin.dsl.bind 8 | import org.koin.dsl.module 9 | 10 | val repositoryModule = module { 11 | singleOf(::SaveGameRepositoryImpl) bind SaveGameRepository::class 12 | 13 | singleOf(::GetHueParamsUseCase) 14 | } 15 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/joaomanaia/game2048/Game2048App.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048 2 | 3 | import android.app.Application 4 | import com.joaomanaia.game2048.di.KoinStarter 5 | import org.koin.android.ext.koin.androidContext 6 | import org.koin.android.ext.koin.androidLogger 7 | import com.google.android.material.math.MathUtils.floorMod 8 | 9 | class Game2048App : Application() { 10 | override fun onCreate() { 11 | super.onCreate() 12 | 13 | KoinStarter.init { 14 | androidLogger() 15 | androidContext(this@Game2048App) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.idea/deploymentTargetSelector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 11 | 12 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/joaomanaia/game2048/presentation/color_settings/ColorSettingsScreenPreview.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.presentation.color_settings 2 | 3 | import androidx.compose.material3.ExperimentalMaterial3Api 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.tooling.preview.PreviewLightDark 6 | import com.joaomanaia.game2048.core.presentation.theme.Game2048Theme 7 | 8 | @Composable 9 | @PreviewLightDark 10 | @OptIn(ExperimentalMaterial3Api::class) 11 | private fun ColorSettingsScreenPreview() { 12 | Game2048Theme { 13 | ColorSettingsScreen( 14 | uiState = ColorSettingsUiState() 15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/presentation/color_settings/ColorSettingsUiState.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.presentation.color_settings 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import com.joaomanaia.game2048.core.presentation.theme.DarkThemeConfig 5 | import com.joaomanaia.game2048.core.presentation.theme.TileColorsGenerator 6 | 7 | data class ColorSettingsUiState( 8 | val darkThemeConfig: DarkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM, 9 | val amoledMode: Boolean = false, 10 | val seedColor: Color? = null, 11 | val hueParams: TileColorsGenerator.HueParams = TileColorsGenerator.HueParams(), 12 | val wallpaperColors: Set = emptySet(), 13 | ) 14 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/presentation/game/GameScreenUiState.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.presentation.game 2 | 3 | import com.joaomanaia.game2048.core.presentation.theme.TileColorsGenerator 4 | import com.joaomanaia.game2048.core.util.emptyGrid 5 | import com.joaomanaia.game2048.model.Grid 6 | import com.joaomanaia.game2048.model.GridTileMovement 7 | 8 | data class GameScreenUiState( 9 | val gridSize: Int = 4, 10 | val grid: Grid = emptyGrid(gridSize), 11 | val gridTileMovements: List = emptyList(), 12 | val currentScore: Int = 0, 13 | val bestScore: Int = 0, 14 | val moveCount: Int = 0, 15 | val isGameOver: Boolean = false, 16 | val hueParams: TileColorsGenerator.HueParams = TileColorsGenerator.HueParams() 17 | ) 18 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/core/datastore/manager/DataStoreManager.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.datastore.manager 2 | 3 | import com.joaomanaia.game2048.core.datastore.Preferences 4 | import com.joaomanaia.game2048.core.datastore.PreferencesKey 5 | import com.joaomanaia.game2048.core.datastore.PreferencesPair 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | interface DataStoreManager { 9 | val preferenceFlow: Flow 10 | 11 | suspend fun getPreference(preferenceEntry: PreferenceRequest): T 12 | 13 | fun getPreferenceFlow(request: PreferenceRequest): Flow 14 | 15 | suspend fun editPreference(key: PreferencesKey, newValue: T) 16 | 17 | suspend fun editPreferences(vararg prefs: PreferencesPair<*>) 18 | 19 | suspend fun clearPreferences() 20 | } 21 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Game 2048 3 | Grid Size 4 | Change grid size to increase or decrease your board size. 5 | %1$d X %1$d 6 | Reset Game 7 | Settings 8 | Score 9 | Best 10 | Game Over 11 | Start a new game? 12 | Back 13 | Starting a new game will erase your current game 14 | OK 15 | Cancel 16 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Game 2048 3 | Grid Size 4 | Change grid size to increase or decrease your board size. 5 | %1$d X %1$d 6 | Reset Game 7 | Settings 8 | Score 9 | Best 10 | Game Over 11 | Start a new game? 12 | Back 13 | Starting a new game will erase your current game 14 | OK 15 | Cancel 16 | -------------------------------------------------------------------------------- /composeApp/src/commonTest/kotlin/com/joaomanaia/game2048/model/TileTest.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.model 2 | 3 | import assertk.assertFailure 4 | import assertk.assertThat 5 | import assertk.assertions.hasMessage 6 | import assertk.assertions.isEqualTo 7 | import assertk.assertions.isInstanceOf 8 | import kotlin.test.Test 9 | 10 | internal class TileTest { 11 | @Test 12 | fun tileNumber_mustBe_aPowerOf2() { 13 | val tile = Tile(16) 14 | 15 | assertThat(tile.num).isEqualTo(16) 16 | assertThat(tile.logNum).isEqualTo(4) 17 | } 18 | 19 | @Test 20 | fun tileNumber_fails_when_incorrectNumProvided() { 21 | val exception = assertFailure { 22 | Tile(15) 23 | } 24 | 25 | exception 26 | .isInstanceOf() 27 | .hasMessage("Tile number must be a power of 2.") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | /app/release 17 | 18 | .kotlin 19 | 20 | **/build/ 21 | xcuserdata 22 | !src/**/build/ 23 | .idea 24 | captures 25 | *.xcodeproj/* 26 | !*.xcodeproj/project.pbxproj 27 | !*.xcodeproj/xcshareddata/ 28 | !*.xcodeproj/project.xcworkspace/ 29 | !*.xcworkspace/contents.xcworkspacedata 30 | **/xcshareddata/WorkspaceSettings.xcsettings 31 | 32 | .idea/* 33 | !.idea/copyright 34 | # Keep the code styles. 35 | !/.idea/codeStyles 36 | /.idea/codeStyles/* 37 | !/.idea/codeStyles/Project.xml 38 | !/.idea/codeStyles/codeStyleConfig.xml 39 | !.idea/runConfigurations/ 40 | 41 | kotlin-js-store 42 | 43 | *.preferences_pb 44 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values-pt/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Jogo 2048 4 | Tamanho da grelha 5 | Altere o tamanho da grelha para aumentar ou diminuir o tamanho da prancha. 6 | Recomeçar Jogo 7 | Definições 8 | Pontuação 9 | Melhor 10 | Fim do jogo 11 | Começar um novo jogo? 12 | Começar um novo jogo vai apagar o teu jogo atual 13 | Voltar 14 | OK 15 | Cancelar 16 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/values-pt/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Jogo 2048 4 | Tamanho da grelha 5 | Altere o tamanho da grelha para aumentar ou diminuir o tamanho da prancha. 6 | Recomeçar Jogo 7 | Definições 8 | Pontuação 9 | Melhor 10 | Fim do jogo 11 | Começar um novo jogo? 12 | Começar um novo jogo vai apagar o teu jogo atual 13 | Voltar 14 | OK 15 | Cancelar 16 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Juego 2048 4 | Tamaño de la cuadrícula 5 | Cambie el tamaño de la cuadrícula para aumentar o disminuir el tamaño de la placa. 6 | Restablecer juego 7 | Configuración 8 | Puntuación 9 | Mejor 10 | Fin 11 | ¿Comenzar un nuevo juego? 12 | Comenzar un nuevo juego borrará tu juego actual 13 | Atrás 14 | OK 15 | Cancelar 16 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/joaomanaia/game2048/di/DataStoreModule.android.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.di 2 | 3 | import android.content.Context 4 | import androidx.datastore.core.DataStore 5 | import androidx.datastore.preferences.core.Preferences 6 | import androidx.datastore.preferences.preferencesDataStore 7 | import com.joaomanaia.game2048.core.datastore.manager.DataStoreManager 8 | import com.joaomanaia.game2048.core.datastore.manager.DataStoreManagerImpl 9 | import org.koin.android.ext.koin.androidContext 10 | import org.koin.core.module.dsl.singleOf 11 | import org.koin.dsl.bind 12 | import org.koin.dsl.module 13 | 14 | val Context.gameDataDataStore: DataStore by preferencesDataStore(name = "game_data") 15 | 16 | actual val dataStoreModule = module { 17 | single { androidContext().gameDataDataStore } 18 | 19 | singleOf(::DataStoreManagerImpl) bind DataStoreManager::class 20 | } 21 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values-fr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Jeu 2048 4 | Taille de la grille 5 | Modifiez la taille de la grille pour augmenter ou diminuer la taille de votre carte. 6 | Réinitialiser le jeu 7 | Paramètres 8 | Score 9 | Meilleur 10 | Fin du jeu 11 | Commencer un nouveau jeu? 12 | Commencer un nouveau jeu effacera votre jeu actuel 13 | Précédent 14 | Annuler 15 | OK 16 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Juego 2048 4 | Tamaño de la cuadrícula 5 | Cambie el tamaño de la cuadrícula para aumentar o disminuir el tamaño de la placa. 6 | Restablecer juego 7 | Configuración 8 | Puntuación 9 | Mejor 10 | Fin 11 | ¿Comenzar un nuevo juego? 12 | Comenzar un nuevo juego borrará tu juego actual 13 | Atrás 14 | OK 15 | Cancelar 16 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/values-fr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Jeu 2048 4 | Taille de la grille 5 | Modifiez la taille de la grille pour augmenter ou diminuer la taille de votre carte. 6 | Réinitialiser le jeu 7 | Paramètres 8 | Score 9 | Meilleur 10 | Fin du jeu 11 | Commencer un nouveau jeu? 12 | Commencer un nouveau jeu effacera votre jeu actuel 13 | Précédent 14 | Annuler 15 | OK 16 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/joaomanaia/game2048/presentation/color_settings/components/DarkThemeDialogPickerPreview.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.presentation.color_settings.components 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.tooling.preview.PreviewLightDark 6 | import com.joaomanaia.game2048.core.presentation.theme.DarkThemeConfig 7 | import com.joaomanaia.game2048.core.presentation.theme.Game2048Theme 8 | 9 | @Composable 10 | @PreviewLightDark 11 | private fun DarkThemeDialogPickerPreview() { 12 | Game2048Theme { 13 | DarkThemeDialogPicker( 14 | useDarkTheme = isSystemInDarkTheme(), 15 | amoledMode = false, 16 | darkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM, 17 | onDarkThemeChanged = {}, 18 | onAmoledModeChanged = {}, 19 | onDismissRequest = {} 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/presentation/color_settings/ColorSettingsUiEvent.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.presentation.color_settings 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import com.joaomanaia.game2048.core.presentation.theme.DarkThemeConfig 5 | 6 | sealed interface ColorSettingsUiEvent { 7 | data class OnDarkThemeChanged(val config: DarkThemeConfig) : ColorSettingsUiEvent 8 | 9 | data class OnAmoledModeChanged(val amoledMode: Boolean) : ColorSettingsUiEvent 10 | 11 | data class OnSeedColorChanged(val color: Color) : ColorSettingsUiEvent 12 | 13 | data class OnIncrementHueChanged(val increment: Boolean) : ColorSettingsUiEvent 14 | 15 | data class OnHueIncrementChanged(val increment: Float) : ColorSettingsUiEvent 16 | 17 | data class OnHueSaturationChanged(val saturation: Float) : ColorSettingsUiEvent 18 | 19 | data class OnHueLightnessChanged(val lightness: Float) : ColorSettingsUiEvent 20 | } 21 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/domain/repository/SaveGameRepository.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.domain.repository 2 | 3 | import com.joaomanaia.game2048.model.Grid 4 | import com.joaomanaia.game2048.model.GridTileMovement 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface SaveGameRepository { 8 | suspend fun checkSaveGameExists(): Boolean 9 | 10 | suspend fun getSavedGrid(): Grid 11 | 12 | suspend fun getSavedGridTileMovements(): List 13 | 14 | suspend fun getSavedCurrentScore(): Int 15 | 16 | suspend fun getSavedBestScore(): Int 17 | 18 | suspend fun getGridSize(): Int 19 | 20 | fun getGridSizeFlow(): Flow 21 | 22 | suspend fun updateGridSize(newSize: Int) 23 | 24 | suspend fun saveGame( 25 | grid: Grid, 26 | currentScore: Int 27 | ) 28 | 29 | suspend fun saveGame( 30 | grid: Grid, 31 | currentScore: Int, 32 | bestScore: Int 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android 2048 Game in jetpack compose 2 | 3 | A simple 2048 game written with 100% Jetpack Compose. 4 | 5 | ## Features 6 | - Jetpack Compose 7 | - Material 3 8 | - MVVM 9 | - Adaptable theme 10 | - Datastore Preferences 11 | - Multiple grid sizes 12 | 13 | With material 3 (material you) from android 12, the app will adapt to your phone's background. 14 | Grid items will automatically harmonize to the theme. 15 | 16 | If you exit the game or want to continue later, all will be saved in datastore. So next time you open the app you have your last game played and your best score. 17 | 18 | ![Screenshot of app](pictures/game2048-anim.gif) 19 | 20 | ## Multiple grid sizes 21 | 22 | Your can select multiple board grid sizes. 23 | All sizes available: 24 | - 3x3 25 | - 4x4 26 | - 5x5 27 | - 6x6 28 | - 7x7 29 | 30 | The default grid size is 4x4, like from the the original game. 31 | 32 | Thanks [@alexjlockwood](https://github.com/alexjlockwood) for the [Original project](https://github.com/alexjlockwood/android-2048-compose) -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/model/Tile.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.model 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlin.math.log2 5 | import kotlin.math.roundToInt 6 | 7 | /** 8 | * Container class that wraps a number and a unique [id] for use in the grid. 9 | */ 10 | @Serializable 11 | data class Tile( 12 | val num: Int, 13 | val id: Int 14 | ) { 15 | init { 16 | // Check if the number is valid, by checking if it is a power of 2. 17 | require(num and (num - 1) == 0) { "Tile number must be a power of 2." } 18 | } 19 | 20 | companion object { 21 | // We assign each tile a unique ID and use it to efficiently 22 | // animate tile objects within the compose UI. 23 | internal var tileIdCounter = 0 24 | } 25 | 26 | constructor(num: Int) : this(num, tileIdCounter++) 27 | 28 | operator fun times(operand: Int): Tile = Tile(num * operand) 29 | 30 | val logNum: Int 31 | get() = log2(num.toFloat()).roundToInt() 32 | } 33 | -------------------------------------------------------------------------------- /.idea/appInsightsSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 25 | 26 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/com/joaomanaia/game2048/di/DataStoreModule.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.di 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.preferences.core.PreferenceDataStoreFactory 5 | import androidx.datastore.preferences.core.Preferences 6 | import com.joaomanaia.game2048.core.datastore.manager.DataStoreManager 7 | import com.joaomanaia.game2048.core.datastore.manager.DataStoreManagerImpl 8 | import okio.Path.Companion.toPath 9 | import org.koin.core.module.dsl.singleOf 10 | import org.koin.dsl.bind 11 | import org.koin.dsl.module 12 | 13 | fun createDataStore(producePath: () -> String): DataStore = 14 | PreferenceDataStoreFactory.createWithPath( 15 | produceFile = { producePath().toPath() } 16 | ) 17 | 18 | internal const val gameDataStoreFileName = "game_data.preferences_pb" 19 | 20 | 21 | actual val dataStoreModule = module { 22 | single { createDataStore { gameDataStoreFileName } } 23 | 24 | singleOf(::DataStoreManagerImpl) bind DataStoreManager::class 25 | } 26 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/core/presentation/theme/Spacing.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.presentation.theme 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.Immutable 6 | import androidx.compose.runtime.ReadOnlyComposable 7 | import androidx.compose.runtime.staticCompositionLocalOf 8 | import androidx.compose.ui.unit.Dp 9 | import androidx.compose.ui.unit.dp 10 | 11 | @Immutable 12 | data class Spacing( 13 | /** 8.dp **/ 14 | val default: Dp = 8.dp, 15 | /** 2.dp **/ 16 | val tiny: Dp = 2.dp, 17 | /** 4.dp **/ 18 | val extraSmall: Dp = 4.dp, 19 | /** 8.dp **/ 20 | val small: Dp = 8.dp, 21 | /** 16.dp **/ 22 | val medium: Dp = 16.dp, 23 | /** 32.dp **/ 24 | val large: Dp = 32.dp, 25 | /** 64.dp **/ 26 | val extraLarge: Dp = 64.dp, 27 | ) 28 | 29 | val LocalSpacing = staticCompositionLocalOf { Spacing() } 30 | 31 | val MaterialTheme.spacing: Spacing 32 | @Composable 33 | @ReadOnlyComposable 34 | get() = LocalSpacing.current 35 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/core/compose/KeyEventHandler.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.compose 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.DisposableEffect 5 | import androidx.compose.runtime.compositionLocalOf 6 | import androidx.compose.runtime.rememberUpdatedState 7 | import androidx.compose.ui.input.key.KeyEvent 8 | 9 | val LocalKeyEventHandlers = compositionLocalOf> { 10 | error("LocalKeyEventHandlers is not provided") 11 | } 12 | 13 | typealias KeyEventHandler = (Int, KeyEvent) -> Boolean 14 | 15 | @Composable 16 | fun ListenKeyEvents(handler: KeyEventHandler) { 17 | val handlerState = rememberUpdatedState(handler) 18 | val eventHandlers = LocalKeyEventHandlers.current 19 | 20 | DisposableEffect(handlerState) { 21 | val localHandler: KeyEventHandler = { keyCode, event -> 22 | handlerState.value(keyCode, event) 23 | } 24 | 25 | eventHandlers.add(localHandler) 26 | 27 | onDispose { 28 | eventHandlers.remove(localHandler) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/presentation/components/BackIconButton.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.presentation.components 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.automirrored.rounded.ArrowBack 5 | import androidx.compose.material3.Icon 6 | import androidx.compose.material3.IconButton 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.navigation.NavController 10 | import androidx.navigation.compose.rememberNavController 11 | 12 | @Composable 13 | internal fun BackIconButton( 14 | modifier: Modifier = Modifier, 15 | navController: NavController = rememberNavController() 16 | ) { 17 | BackIconButton( 18 | modifier = modifier, 19 | onClick = navController::navigateUp 20 | ) 21 | } 22 | 23 | @Composable 24 | internal fun BackIconButton( 25 | modifier: Modifier = Modifier, 26 | onClick: () -> Unit 27 | ) { 28 | IconButton( 29 | modifier = modifier, 30 | onClick = onClick 31 | ) { 32 | Icon( 33 | imageVector = Icons.AutoMirrored.Rounded.ArrowBack, 34 | contentDescription = "Back" 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/kotlin/com/joaomanaia/game2048/core/presentation/theme/Theme.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.presentation.theme 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.CompositionLocalProvider 6 | import androidx.compose.ui.graphics.Color 7 | import com.materialkolor.rememberDynamicColorScheme 8 | 9 | @Composable 10 | actual fun Game2048Theme( 11 | seedColor: Color?, 12 | useDarkTheme: Boolean, 13 | amoledMode: Boolean, 14 | isDynamic: Boolean, // Unused parameter 15 | content: @Composable () -> Unit 16 | ) { 17 | val colorScheme = when { 18 | seedColor != null -> { 19 | rememberDynamicColorScheme( 20 | seedColor = seedColor, 21 | isDark = useDarkTheme, 22 | isAmoled = amoledMode 23 | ) 24 | } 25 | useDarkTheme -> DarkThemeColors 26 | else -> LightThemeColors 27 | } 28 | 29 | CompositionLocalProvider( 30 | LocalSpacing provides Spacing(), 31 | ) { 32 | MaterialTheme( 33 | colorScheme = colorScheme, 34 | typography = AppTypography, 35 | content = content 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/com/joaomanaia/game2048/core/presentation/theme/Theme.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.presentation.theme 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.CompositionLocalProvider 6 | import androidx.compose.ui.graphics.Color 7 | import com.materialkolor.rememberDynamicColorScheme 8 | 9 | @Composable 10 | actual fun Game2048Theme( 11 | seedColor: Color?, 12 | useDarkTheme: Boolean, 13 | amoledMode: Boolean, 14 | isDynamic: Boolean, // Unused parameter 15 | content: @Composable () -> Unit 16 | ) { 17 | val colorScheme = when { 18 | seedColor != null -> { 19 | rememberDynamicColorScheme( 20 | seedColor = seedColor, 21 | isDark = useDarkTheme, 22 | isAmoled = amoledMode 23 | ) 24 | } 25 | useDarkTheme -> DarkThemeColors 26 | else -> LightThemeColors 27 | } 28 | 29 | CompositionLocalProvider( 30 | LocalSpacing provides Spacing(), 31 | ) { 32 | MaterialTheme( 33 | colorScheme = colorScheme, 34 | typography = AppTypography, 35 | content = content 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/joaomanaia/game2048/presentation/color_settings/components/PaletteItemPreview.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.presentation.color_settings.components 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.foundation.layout.size 5 | import androidx.compose.material3.Surface 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.Color 9 | import androidx.compose.ui.tooling.preview.PreviewLightDark 10 | import androidx.compose.ui.tooling.preview.PreviewParameter 11 | import androidx.compose.ui.unit.dp 12 | import com.joaomanaia.game2048.core.compose.preview.BooleanPreviewProvider 13 | import com.joaomanaia.game2048.core.presentation.theme.Game2048Theme 14 | 15 | 16 | @Composable 17 | @PreviewLightDark 18 | private fun PaletteItemPreview( 19 | @PreviewParameter(BooleanPreviewProvider::class) selected: Boolean 20 | ) { 21 | Game2048Theme { 22 | Surface { 23 | SelectablePaletteItem( 24 | modifier = Modifier 25 | .padding(8.dp) 26 | .size(75.dp), 27 | selected = selected, 28 | baseColor = Color.Blue, 29 | onClick = {} 30 | ) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/domain/usecase/GetHueParamsUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.domain.usecase 2 | 3 | import com.joaomanaia.game2048.core.common.preferences.GameDataPreferencesCommon 4 | import com.joaomanaia.game2048.core.datastore.manager.DataStoreManager 5 | import com.joaomanaia.game2048.core.presentation.theme.TileColorsGenerator 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.coroutines.flow.combine 8 | 9 | class GetHueParamsUseCase( 10 | private val gameDataStoreManager: DataStoreManager 11 | ) { 12 | operator fun invoke(): Flow = combine( 13 | gameDataStoreManager.getPreferenceFlow(GameDataPreferencesCommon.IncrementHue), 14 | gameDataStoreManager.getPreferenceFlow(GameDataPreferencesCommon.HueIncrementValue), 15 | gameDataStoreManager.getPreferenceFlow(GameDataPreferencesCommon.HueSaturation), 16 | gameDataStoreManager.getPreferenceFlow(GameDataPreferencesCommon.HueLightness) 17 | ) { incrementHue, hueIncrement, hueSaturation, hueLightness -> 18 | TileColorsGenerator.HueParams( 19 | isIncrement = incrementHue, 20 | hueIncrement = hueIncrement, 21 | saturation = hueSaturation, 22 | lightness = hueLightness 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/presentation/components/GameDialog.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.presentation.components 2 | 3 | import androidx.compose.material3.AlertDialog 4 | import androidx.compose.material3.Text 5 | import androidx.compose.material3.TextButton 6 | import androidx.compose.runtime.Composable 7 | import game2048.composeapp.generated.resources.Res 8 | import game2048.composeapp.generated.resources.cancel 9 | import game2048.composeapp.generated.resources.ok 10 | import org.jetbrains.compose.resources.stringResource 11 | 12 | @Composable 13 | fun GameDialog( 14 | title: String, 15 | message: String, 16 | confirmText: String = stringResource(Res.string.ok), 17 | dismissText: String = stringResource(Res.string.cancel), 18 | onConfirmListener: () -> Unit, 19 | onDismissListener: () -> Unit 20 | ) { 21 | AlertDialog( 22 | title = { Text(text = title) }, 23 | text = { Text(text = message) }, 24 | confirmButton = { 25 | TextButton(onClick = onConfirmListener) { 26 | Text(text = confirmText) 27 | } 28 | }, 29 | dismissButton = { 30 | TextButton(onClick = onDismissListener) { 31 | Text(text = dismissText) 32 | } 33 | }, 34 | onDismissRequest = onDismissListener 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/joaomanaia/game2048/presentation/game/components/grid/GridTitleTextPreview.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.presentation.game.components.grid 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.foundation.layout.size 5 | import androidx.compose.material3.Surface 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.geometry.Offset 9 | import androidx.compose.ui.text.TextStyle 10 | import androidx.compose.ui.tooling.preview.PreviewLightDark 11 | import androidx.compose.ui.unit.dp 12 | import androidx.compose.ui.unit.sp 13 | import com.joaomanaia.game2048.core.presentation.theme.Game2048Theme 14 | import com.joaomanaia.game2048.model.Tile 15 | 16 | @Composable 17 | @PreviewLightDark 18 | private fun GridTitleTextPreview() { 19 | Game2048Theme { 20 | Surface { 21 | GridTileText( 22 | modifier = Modifier 23 | .padding(16.dp) 24 | .size(100.dp), 25 | tile = Tile(2), 26 | fromScale = 0f, 27 | fromOffset = Offset(0f, 0f), 28 | toOffset = Offset(0f, 0f), 29 | moveCount = 0, 30 | textStyle = TextStyle( 31 | fontSize = 30.sp 32 | ) 33 | ) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /composeApp/src/commonTest/kotlin/com/joaomanaia/game2048/core/util/ListRotationTest.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.util 2 | 3 | import assertk.assertThat 4 | import assertk.assertions.isEmpty 5 | import assertk.assertions.isEqualTo 6 | import kotlin.test.Test 7 | 8 | internal class ListRotationTest { 9 | @Test 10 | fun test_emptyList() { 11 | val emptyList = emptyList>() 12 | val rotatedList = emptyList.rotate(numRotations = 1) 13 | 14 | assertThat(rotatedList).isEmpty() 15 | assertThat(rotatedList).isEqualTo(emptyList) 16 | } 17 | 18 | @Test 19 | fun test_singleElementList() { 20 | val singleElementList = listOf(listOf(1)) 21 | 22 | repeat(4) { 23 | val rotatedList = singleElementList.rotate(numRotations = it) 24 | assertThat(rotatedList).isEqualTo(singleElementList) 25 | } 26 | } 27 | 28 | @Test 29 | fun test_90DegreeRotationOnSquareGrid() { 30 | val originalList = listOf( 31 | listOf(1, 2, 3), 32 | listOf(4, 5, 6), 33 | listOf(7, 8, 9) 34 | ) 35 | val expectedList = listOf( 36 | listOf(7, 4, 1), 37 | listOf(8, 5, 2), 38 | listOf(9, 6, 3) 39 | ) 40 | val rotatedList = originalList.rotate(numRotations = 1) 41 | assertThat(rotatedList).isEqualTo(expectedList) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/core/datastore/PreferencesKey.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.datastore 2 | 3 | expect abstract class Preferences internal constructor() { 4 | abstract fun asMap(): Map, Any> 5 | 6 | abstract operator fun contains(key: PreferencesKey): Boolean 7 | 8 | abstract operator fun get(key: PreferencesKey): T? 9 | 10 | fun toMutablePreferences(): MutablePreferences 11 | 12 | fun toPreferences(): Preferences 13 | } 14 | 15 | expect class PreferencesKey internal constructor(name: String) { 16 | val name: String 17 | 18 | override operator fun equals(other: Any?): Boolean 19 | 20 | override fun hashCode(): Int 21 | 22 | infix fun to(value: T): PreferencesPair 23 | 24 | override fun toString(): String 25 | } 26 | 27 | expect class PreferencesPair internal constructor(key: PreferencesKey, value: T) { 28 | internal val key: PreferencesKey 29 | 30 | internal val value: T 31 | } 32 | 33 | 34 | expect class MutablePreferences : Preferences 35 | 36 | fun booleanPreferencesKey(name: String): PreferencesKey = PreferencesKey(name) 37 | 38 | fun intPreferencesKey(name: String): PreferencesKey = PreferencesKey(name) 39 | 40 | fun floatPreferencesKey(name: String): PreferencesKey = PreferencesKey(name) 41 | 42 | fun stringPreferencesKey(name: String): PreferencesKey = PreferencesKey(name) 43 | 44 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/model/GridTileMovement.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.model 2 | 3 | /** 4 | * Container class describing how a tile has moved within the grid. 5 | */ 6 | data class GridTileMovement( 7 | val fromGridTile: GridTile?, 8 | val toGridTile: GridTile 9 | ) { 10 | companion object { 11 | /** 12 | * Creates a [GridTileMovement] describing a tile that has been added to the grid. 13 | */ 14 | fun add(gridTile: GridTile): GridTileMovement { 15 | return GridTileMovement(null, gridTile) 16 | } 17 | /** 18 | * Creates a [GridTileMovement] describing a tile that has shifted to a different location in the grid. 19 | */ 20 | fun shift(fromGridTile: GridTile, toGridTile: GridTile): GridTileMovement { 21 | return GridTileMovement(fromGridTile, toGridTile) 22 | } 23 | 24 | /** 25 | * Creates a [GridTileMovement] describing a tile that has not moved in the grid. 26 | */ 27 | fun noop(gridTile: GridTile): GridTileMovement { 28 | return GridTileMovement(gridTile, gridTile) 29 | } 30 | 31 | } 32 | 33 | override fun toString(): String { 34 | return when (fromGridTile) { 35 | null -> "add $toGridTile" 36 | toGridTile -> "noop $fromGridTile" 37 | else -> "shift $fromGridTile -> $toGridTile" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/core/util/ListUtil.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.util 2 | 3 | import androidx.annotation.IntRange 4 | import com.joaomanaia.game2048.model.Cell 5 | 6 | internal fun List>.map( 7 | transform: ( 8 | row: Int, 9 | col: Int, 10 | T 11 | ) -> T 12 | ): List> = mapIndexed { row, rowTiles -> 13 | rowTiles.mapIndexed { col, colTile -> 14 | transform(row, col, colTile) 15 | } 16 | } 17 | 18 | internal fun List>.rotate( 19 | @IntRange(from = 0, to = 3) numRotations: Int 20 | ): List> { 21 | require(numRotations in 0..3) { "numRotations must be an integer in 0..3" } 22 | 23 | return map { row, col, _ -> 24 | val (rotatedRow, rotatedCol) = getRotatedCellAt( 25 | row = row, 26 | col = col, 27 | numRotations = numRotations, 28 | gridSize = size 29 | ) 30 | this[rotatedRow][rotatedCol] 31 | } 32 | } 33 | 34 | internal fun getRotatedCellAt( 35 | row: Int, 36 | col: Int, 37 | @IntRange(from = 0, to = 3) numRotations: Int, 38 | gridSize: Int 39 | ): Cell { 40 | return when (numRotations) { 41 | 0 -> Cell(row, col) 42 | 1 -> Cell(gridSize - 1 - col, row) 43 | 2 -> Cell(gridSize - 1 - row, gridSize - 1 - col) 44 | 3 -> Cell(col, gridSize - 1 - row) 45 | else -> throw IllegalArgumentException("numRotations must be a integer in 0..3") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /composeApp/src/androidJvmMain/kotlin/com/joaomanaia/game2048/core/datastore/manager/DataStoreManagerImpl.androidJvm.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.datastore.manager 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.preferences.core.Preferences 5 | import androidx.datastore.preferences.core.edit 6 | import kotlinx.coroutines.flow.distinctUntilChanged 7 | import kotlinx.coroutines.flow.firstOrNull 8 | import kotlinx.coroutines.flow.map 9 | 10 | actual class DataStoreManagerImpl( 11 | private val dataStore: DataStore 12 | ) : DataStoreManager { 13 | override val preferenceFlow = dataStore.data 14 | 15 | override suspend fun getPreference( 16 | preferenceEntry: PreferenceRequest 17 | ) = preferenceFlow 18 | .firstOrNull() 19 | ?.get(preferenceEntry.key) ?: preferenceEntry.defaultValue 20 | 21 | override fun getPreferenceFlow(request: PreferenceRequest) = preferenceFlow.map { 22 | it[request.key] ?: request.defaultValue 23 | }.distinctUntilChanged() 24 | 25 | override suspend fun editPreference(key: Preferences.Key, newValue: T) { 26 | dataStore.edit { preferences -> preferences[key] = newValue } 27 | } 28 | 29 | override suspend fun editPreferences(vararg prefs: Preferences.Pair<*>) { 30 | dataStore.edit { preferences -> 31 | prefs.forEach { 32 | preferences.plusAssign(it) 33 | } 34 | } 35 | } 36 | 37 | override suspend fun clearPreferences() { 38 | dataStore.edit { preferences -> preferences.clear() } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/joaomanaia/game2048/presentation/color_settings/components/BaseColorChooserPreview.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.presentation.color_settings.components 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.Surface 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.runtime.setValue 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.tooling.preview.PreviewLightDark 15 | import androidx.compose.ui.unit.dp 16 | import com.joaomanaia.game2048.core.presentation.theme.Game2048Theme 17 | 18 | @Composable 19 | @PreviewLightDark 20 | private fun BaseColorChooserPreview() { 21 | Game2048Theme { 22 | Surface { 23 | Column( 24 | modifier = Modifier.padding(16.dp) 25 | ) { 26 | var color by remember { 27 | mutableStateOf(Color.Yellow) 28 | } 29 | 30 | BaseColorChooser( 31 | useDarkTheme = isSystemInDarkTheme(), 32 | currentSeedColor = color, 33 | wallpaperColors = emptySet(), 34 | defaultSelectedTab = PreviewTabButtonType.BASIC_COLOR, 35 | onColorSelected = { color = it } 36 | ) 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ "main" ] 7 | paths-ignore: 8 | - '**.md' 9 | pull_request: 10 | branches: [ "main" ] 11 | paths-ignore: 12 | - '**.md' 13 | 14 | permissions: 15 | contents: read 16 | checks: write 17 | pages: write 18 | id-token: write 19 | 20 | concurrency: 21 | group: "build" 22 | cancel-in-progress: false 23 | 24 | env: 25 | JAVA_VERSION: "17" 26 | JAVA_DISTR: 'corretto' 27 | 28 | jobs: 29 | package-web: 30 | name: "📦 Build Web" 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout sources 34 | uses: actions/checkout@v4 35 | 36 | - name: Setup Pages 37 | uses: actions/configure-pages@v4 38 | 39 | - name: Set up JDK 40 | uses: actions/setup-java@v3 41 | with: 42 | distribution: ${{ env.JAVA_DISTR }} 43 | java-version: ${{ env.JAVA_VERSION }} 44 | 45 | - name: Setup gradle 46 | uses: gradle/actions/setup-gradle@v3 47 | 48 | - name: Build Web 49 | run: ./gradlew wasmJsBrowserDistribution --stacktrace 50 | 51 | - name: Upload Web artifacts 52 | uses: actions/upload-pages-artifact@v3 53 | with: 54 | path: ./composeApp/build/dist/wasmJs/productionExecutable/ 55 | 56 | deploy-web: 57 | name: "🚀 Deploy Web" 58 | runs-on: ubuntu-latest 59 | environment: 60 | name: gh-pages 61 | url: ${{ steps.deployment.outputs.page_url }} 62 | needs: 63 | - package-web 64 | steps: 65 | - name: Deploy to GitHub Pages 66 | id: deployment 67 | uses: actions/deploy-pages@v4 68 | -------------------------------------------------------------------------------- /composeApp/build/generated/compose/resourceGenerator/kotlin/commonResClass/game2048/composeapp/generated/resources/Res.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn( 2 | org.jetbrains.compose.resources.InternalResourceApi::class, 3 | org.jetbrains.compose.resources.ExperimentalResourceApi::class, 4 | ) 5 | 6 | package game2048.composeapp.generated.resources 7 | 8 | import kotlin.ByteArray 9 | import kotlin.OptIn 10 | import kotlin.String 11 | import org.jetbrains.compose.resources.ExperimentalResourceApi 12 | import org.jetbrains.compose.resources.getResourceUri 13 | import org.jetbrains.compose.resources.readResourceBytes 14 | 15 | internal object Res { 16 | /** 17 | * Reads the content of the resource file at the specified path and returns it as a byte array. 18 | * 19 | * Example: `val bytes = Res.readBytes("files/key.bin")` 20 | * 21 | * @param path The path of the file to read in the compose resource's directory. 22 | * @return The content of the file as a byte array. 23 | */ 24 | @ExperimentalResourceApi 25 | public suspend fun readBytes(path: String): ByteArray = 26 | readResourceBytes("composeResources/game2048.composeapp.generated.resources/" + path) 27 | 28 | /** 29 | * Returns the URI string of the resource file at the specified path. 30 | * 31 | * Example: `val uri = Res.getUri("files/key.bin")` 32 | * 33 | * @param path The path of the file in the compose resource's directory. 34 | * @return The URI string of the file. 35 | */ 36 | @ExperimentalResourceApi 37 | public fun getUri(path: String): String = 38 | getResourceUri("composeResources/game2048.composeapp.generated.resources/" + path) 39 | 40 | public object drawable 41 | 42 | public object string 43 | 44 | public object array 45 | 46 | public object plurals 47 | 48 | public object font 49 | } 50 | -------------------------------------------------------------------------------- /composeApp/src/commonTest/kotlin/com/joaomanaia/game2048/model/HslColorTest.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.model 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.toArgb 5 | import assertk.assertThat 6 | import assertk.assertions.isCloseTo 7 | import kotlin.test.Test 8 | 9 | internal class HslColorTest { 10 | companion object { 11 | private const val TOLERANCE = 0.01f 12 | 13 | private data class Argument( 14 | val colorArgb: Int, 15 | val expectedHue: Float, 16 | val expectedSaturation: Float, 17 | val expectedLightness: Float 18 | ) 19 | 20 | private fun colorToHslProvider() = listOf( 21 | Argument(Color.Red.toArgb(), 0f, 1f, 0.5f), 22 | Argument(Color.Green.toArgb(), 120f, 1f, 0.5f), 23 | Argument(Color.Blue.toArgb(), 240f, 1f, 0.5f), 24 | Argument(Color.Black.toArgb(), 0f, 0f, 0f), 25 | Argument(Color.White.toArgb(), 0f, 0f, 1f), 26 | Argument(Color(0.5f, 0.5f, 0.5f).toArgb(), 0f, 0f, 0.5f), 27 | Argument(Color.Yellow.toArgb(), 60f, 1f, 0.5f), 28 | Argument(Color.Cyan.toArgb(), 180f, 1f, 0.5f), 29 | Argument(Color.Magenta.toArgb(), 300f, 1f, 0.5f) 30 | ) 31 | } 32 | 33 | @Test 34 | fun test_toHsl() { 35 | colorToHslProvider().forEach { (colorArgb, expectedHue, expectedSaturation, expectedLightness) -> 36 | val hsl = Color(colorArgb).toHsl() 37 | 38 | assertThat(expectedHue, "hue").isCloseTo(hsl.hue, TOLERANCE) 39 | assertThat(expectedSaturation, "saturation").isCloseTo(hsl.saturation, TOLERANCE) 40 | assertThat(expectedLightness, "lightness").isCloseTo(hsl.lightness, TOLERANCE) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/presentation/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.presentation 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.joaomanaia.game2048.core.common.preferences.GameDataPreferencesCommon 7 | import com.joaomanaia.game2048.core.datastore.manager.DataStoreManager 8 | import com.joaomanaia.game2048.core.presentation.theme.DarkThemeConfig 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.SharingStarted 11 | import kotlinx.coroutines.flow.combine 12 | import kotlinx.coroutines.flow.map 13 | import kotlinx.coroutines.flow.stateIn 14 | 15 | class MainViewModel( 16 | private val gameDataStoreManager: DataStoreManager 17 | ) : ViewModel() { 18 | val uiState = combine( 19 | getDarkThemeConfigFlow(), 20 | gameDataStoreManager.getPreferenceFlow(GameDataPreferencesCommon.AmoledMode), 21 | getSeedColorFlow() 22 | ) { darkThemeConfig, amoledMode, seedColor -> 23 | MainUiState.Success( 24 | darkThemeConfig = darkThemeConfig, 25 | amoledMode = amoledMode, 26 | seedColor = seedColor 27 | ) 28 | }.stateIn( 29 | scope = viewModelScope, 30 | started = SharingStarted.WhileSubscribed(5000), 31 | initialValue = MainUiState.Loading 32 | ) 33 | 34 | private fun getDarkThemeConfigFlow() = gameDataStoreManager 35 | .getPreferenceFlow(GameDataPreferencesCommon.DarkThemeConfig) 36 | .map(DarkThemeConfig::valueOf) 37 | 38 | private fun getSeedColorFlow(): Flow = gameDataStoreManager 39 | .getPreferenceFlow(GameDataPreferencesCommon.SeedColor) 40 | .map { colorArgb -> 41 | if (colorArgb != -1) Color(colorArgb) else null 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /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 24 | 25 | # Enable Gradle Daemon 26 | org.gradle.daemon=true 27 | 28 | # Enable R8 full mode 29 | android.enableR8.fullMode = true 30 | 31 | # Turn on parallel compilation, caching and on-demand configuration 32 | org.gradle.configureondemand=true 33 | org.gradle.caching=true 34 | org.gradle.parallel=true 35 | 36 | kotlin.incremental=true 37 | 38 | org.gradle.unsafe.configuration-cache=true 39 | android.nonFinalResIds=false 40 | 41 | kotlin.mpp.androidGradlePluginCompatibility.nowarn=true 42 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/model/HslColor.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.model 2 | 3 | import androidx.annotation.FloatRange 4 | import androidx.compose.ui.graphics.Color 5 | import androidx.compose.ui.graphics.colorspace.ColorSpaces 6 | import kotlin.math.abs 7 | 8 | /** 9 | * Represents an HSL color with hue, saturation, and lightness values. 10 | * 11 | * @param hue The hue value in degrees (0-360). 12 | * @param saturation The saturation value (0-1). 13 | * @param lightness The lightness value (0-1). 14 | */ 15 | data class HslColor( 16 | @FloatRange(from = 0.0, to = 360.0) val hue: Float, 17 | @FloatRange(from = 0.0, to = 1.0) val saturation: Float, 18 | @FloatRange(from = 0.0, to = 1.0) val lightness: Float 19 | ) { 20 | init { 21 | require(hue in 0.0..360.0) { "Hue must be between 0 and 360 but was $hue" } 22 | require(saturation in 0.0..1.0) { "Saturation must be between 0 and 1 but was $saturation" } 23 | require(lightness in 0.0..1.0) { "Lightness must be between 0 and 1 but was $lightness" } 24 | } 25 | } 26 | 27 | internal fun Color.toHsl(): HslColor { 28 | val srgbColor = convert(ColorSpaces.Srgb) 29 | 30 | val rf = srgbColor.red 31 | val gf = srgbColor.green 32 | val bf = srgbColor.blue 33 | 34 | val max = maxOf(rf, gf, bf) 35 | val min = minOf(rf, gf, bf) 36 | val deltaMaxMin = max - min 37 | 38 | var h: Float 39 | val s: Float 40 | val l = (max + min) / 2f 41 | 42 | if (max == min) { 43 | // Monochromatic 44 | s = 0f 45 | h = s 46 | } else { 47 | h = when (max) { 48 | rf -> (gf - bf) / deltaMaxMin % 6f 49 | gf -> (bf - rf) / deltaMaxMin + 2f 50 | else -> (rf - gf) / deltaMaxMin + 4f 51 | } 52 | 53 | s = (deltaMaxMin / (1f - abs((2f * l - 1f).toDouble()))).toFloat() 54 | } 55 | 56 | h = (h * 60f) % 360f 57 | if (h < 0) { 58 | h += 360f 59 | } 60 | 61 | return HslColor( 62 | hue = h.coerceIn(0f, 360f), 63 | saturation = s.coerceIn(0f, 1f), 64 | lightness = l.coerceIn(0f, 1f) 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/joaomanaia/game2048/core/presentation/theme/Theme.android.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.presentation.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.dynamicDarkColorScheme 7 | import androidx.compose.material3.dynamicLightColorScheme 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.CompositionLocalProvider 10 | import androidx.compose.runtime.SideEffect 11 | import androidx.compose.ui.graphics.Color 12 | import androidx.compose.ui.graphics.toArgb 13 | import androidx.compose.ui.platform.LocalContext 14 | import androidx.compose.ui.platform.LocalView 15 | import androidx.core.view.WindowCompat 16 | import com.materialkolor.rememberDynamicColorScheme 17 | 18 | @Composable 19 | actual fun Game2048Theme( 20 | seedColor: Color?, 21 | useDarkTheme: Boolean, 22 | amoledMode: Boolean, 23 | isDynamic: Boolean, 24 | content: @Composable () -> Unit 25 | ) { 26 | val colorScheme = when { 27 | seedColor != null -> { 28 | rememberDynamicColorScheme( 29 | seedColor = seedColor, 30 | isDark = useDarkTheme, 31 | isAmoled = amoledMode 32 | ) 33 | } 34 | isDynamic && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 35 | val context = LocalContext.current 36 | if (useDarkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 37 | } 38 | 39 | useDarkTheme -> DarkThemeColors 40 | else -> LightThemeColors 41 | } 42 | 43 | val view = LocalView.current 44 | if (!view.isInEditMode) { 45 | val currentWindow = (view.context as? Activity)?.window 46 | ?: throw Exception("Not in an activity - unable to get Window reference") 47 | 48 | SideEffect { 49 | currentWindow.statusBarColor = colorScheme.primary.toArgb() 50 | 51 | WindowCompat 52 | .getInsetsController(currentWindow, view) 53 | .isAppearanceLightStatusBars = useDarkTheme 54 | } 55 | } 56 | 57 | CompositionLocalProvider( 58 | LocalSpacing provides Spacing(), 59 | ) { 60 | MaterialTheme( 61 | colorScheme = colorScheme, 62 | typography = AppTypography, 63 | content = content 64 | ) 65 | } 66 | } -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/core/presentation/theme/TileColorsGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.presentation.theme 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.ui.graphics.Color 5 | import com.joaomanaia.game2048.model.Tile 6 | import com.joaomanaia.game2048.model.toHsl 7 | 8 | object TileColorsGenerator { 9 | const val DEFAULT_INCREMENT_HUE = false 10 | const val DEFAULT_HUE_INCREMENT = 12f 11 | const val DEFAULT_SATURATION = 0.5f 12 | const val DEFAULT_LIGHTNESS = 0.5f 13 | 14 | @Immutable 15 | data class HueParams( 16 | val hueIncrement: Float = DEFAULT_HUE_INCREMENT, 17 | val isIncrement: Boolean = DEFAULT_INCREMENT_HUE, 18 | val saturation: Float = DEFAULT_SATURATION, 19 | val lightness: Float = DEFAULT_LIGHTNESS 20 | ) 21 | 22 | fun getColorForTile( 23 | tile: Tile, 24 | baseColor: Color, 25 | hueParams: HueParams = HueParams() 26 | ): Color = getColorForTile(tile.logNum, baseColor, hueParams) 27 | 28 | fun getColorForTile( 29 | logNum: Int, 30 | baseColor: Color, 31 | hueParams: HueParams = HueParams() 32 | ): Color { 33 | // Calculate the hue increment based on the log number of the tile, 34 | // using the index of the tile in the sequence. 35 | val tileHueIncrement = (logNum - 1) * hueParams.hueIncrement 36 | val baseHue = baseColor.toHsl().hue 37 | 38 | val hue = calculateHue(baseHue, tileHueIncrement, hueParams.isIncrement) 39 | 40 | return Color.hsl(hue, hueParams.saturation, hueParams.lightness) 41 | } 42 | 43 | /** 44 | * Generates a sequence of colors by incrementing or decrementing the hue of the given base color. 45 | * 46 | * @param baseColor The color to start with. 47 | * @return A sequence of colors with changing hue values. 48 | */ 49 | fun generateHueSequence( 50 | baseColor: Color, 51 | hueParams: HueParams = HueParams() 52 | ): Sequence = generateSequence(baseColor.toHsl().hue) { hue -> 53 | calculateHue(hue, hueParams.hueIncrement, hueParams.isIncrement) 54 | }.map { Color.hsl(it, hueParams.saturation, hueParams.lightness) } 55 | 56 | private fun calculateHue( 57 | hue: Float, 58 | hueIncrement: Float, 59 | isIncrement: Boolean 60 | ): Float { 61 | return if (isIncrement) { 62 | (hue + hueIncrement) % 360f 63 | } else { 64 | (hue - hueIncrement + 360f) % 360f 65 | }.coerceIn(0f, 360f) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/core/common/preferences/GameDataPreferencesCommon.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.common.preferences 2 | 3 | import com.joaomanaia.game2048.core.datastore.booleanPreferencesKey 4 | import com.joaomanaia.game2048.core.datastore.floatPreferencesKey 5 | import com.joaomanaia.game2048.core.datastore.intPreferencesKey 6 | import com.joaomanaia.game2048.core.datastore.manager.PreferenceRequest 7 | import com.joaomanaia.game2048.core.datastore.stringPreferencesKey 8 | import com.joaomanaia.game2048.core.presentation.theme.TileColorsGenerator 9 | import com.joaomanaia.game2048.core.presentation.theme.DarkThemeConfig as EnumDarkThemeConfig 10 | 11 | object GameDataPreferencesCommon { 12 | object Grid : PreferenceRequest(stringPreferencesKey("grid"), "[]") 13 | 14 | object CurrentScore : PreferenceRequest(intPreferencesKey("current_score"), 0) 15 | 16 | object BestScore : PreferenceRequest(intPreferencesKey("best_score"), 0) 17 | 18 | object GridSize : PreferenceRequest(intPreferencesKey("grid_size"), 4) 19 | 20 | // Theme 21 | object DarkThemeConfig : PreferenceRequest( 22 | key = stringPreferencesKey("dark_theme_config"), 23 | defaultValue = EnumDarkThemeConfig.FOLLOW_SYSTEM.name 24 | ) 25 | 26 | object AmoledMode : PreferenceRequest( 27 | key = booleanPreferencesKey("amoled_mode"), 28 | defaultValue = false 29 | ) 30 | 31 | object SeedColor : PreferenceRequest( 32 | key = intPreferencesKey("seed_color"), 33 | defaultValue = -1 34 | ) 35 | 36 | object IncrementHue : PreferenceRequest( 37 | key = booleanPreferencesKey("increment_hue"), 38 | defaultValue = TileColorsGenerator.DEFAULT_INCREMENT_HUE 39 | ) 40 | 41 | object HueIncrementValue : PreferenceRequest( 42 | key = floatPreferencesKey("hue_increment_value"), 43 | defaultValue = TileColorsGenerator.DEFAULT_HUE_INCREMENT 44 | ) 45 | 46 | object HueSaturation : PreferenceRequest( 47 | key = floatPreferencesKey("hue_saturation"), 48 | defaultValue = TileColorsGenerator.DEFAULT_SATURATION 49 | ) 50 | 51 | object HueLightness : PreferenceRequest( 52 | key = floatPreferencesKey("hue_lightness"), 53 | defaultValue = TileColorsGenerator.DEFAULT_LIGHTNESS 54 | ) 55 | 56 | internal fun allPreferences() = listOf( 57 | Grid, 58 | CurrentScore, 59 | BestScore, 60 | GridSize, 61 | DarkThemeConfig, 62 | AmoledMode, 63 | SeedColor, 64 | IncrementHue, 65 | HueIncrementValue, 66 | HueSaturation, 67 | HueLightness 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/presentation/game/components/grid/GridContainer.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.presentation.game.components.grid 2 | 3 | import androidx.compose.foundation.layout.BoxWithConstraints 4 | import androidx.compose.foundation.layout.BoxWithConstraintsScope 5 | import androidx.compose.foundation.layout.aspectRatio 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.UiComposable 10 | import androidx.compose.ui.draw.drawBehind 11 | import androidx.compose.ui.geometry.CornerRadius 12 | import androidx.compose.ui.geometry.Offset 13 | import androidx.compose.ui.geometry.Size 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.platform.LocalDensity 16 | import androidx.compose.ui.unit.Dp 17 | import androidx.compose.ui.unit.TextUnit 18 | import androidx.compose.ui.unit.dp 19 | 20 | @Composable 21 | internal fun GridContainer( 22 | modifier: Modifier = Modifier, 23 | gridSize: Int, 24 | content: @Composable @UiComposable BoxWithConstraintsScope.( 25 | tileSize: Dp, 26 | tileOffsetPx: Float, 27 | textFontSize: TextUnit 28 | ) -> Unit 29 | ) { 30 | BoxWithConstraints( 31 | modifier = modifier.aspectRatio(1f), 32 | ) { 33 | val tileWidth = remember(maxWidth, gridSize) { 34 | calculateTileSize(maxWidth, gridSize) 35 | } 36 | 37 | val tileOffsetPx = with(LocalDensity.current) { 38 | tileWidth.toPx() + GRID_ITEM_GAP.toPx() 39 | } 40 | 41 | val textFontSize = with(LocalDensity.current) { 42 | (tileWidth * 0.3f).toSp() 43 | } 44 | 45 | content(tileWidth, tileOffsetPx, textFontSize) 46 | } 47 | } 48 | 49 | internal fun Modifier.drawBackgroundGrid( 50 | gridSize: Int, 51 | emptyTileColor: Color 52 | ) = drawBehind { 53 | val tileSizePx = calculateTileSize(size.width.toDp(), gridSize).toPx() 54 | val tileOffsetPx = tileSizePx + GRID_ITEM_GAP.toPx() 55 | 56 | // Draw the background empty tiles. 57 | for (row in 0 until gridSize) { 58 | for (col in 0 until gridSize) { 59 | drawRoundRect( 60 | color = emptyTileColor, 61 | topLeft = Offset( 62 | x = col * tileOffsetPx, 63 | y = row * tileOffsetPx 64 | ), 65 | size = Size(tileSizePx, tileSizePx), 66 | cornerRadius = CornerRadius(GRID_TILE_RADIUS.toPx()), 67 | ) 68 | } 69 | } 70 | } 71 | 72 | private fun calculateTileSize(maxWidth: Dp, gridSize: Int): Dp { 73 | return (maxWidth - GRID_ITEM_GAP * (gridSize - 1)) / gridSize 74 | } 75 | 76 | private val GRID_ITEM_GAP = 6.dp 77 | private val GRID_TILE_RADIUS = 4.dp 78 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/joaomanaia/game2048/core/presentation/theme/WallpaperColors.android.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.presentation.theme 2 | 3 | import android.app.WallpaperManager 4 | import android.content.Context 5 | import android.os.Build 6 | import androidx.annotation.RequiresApi 7 | import androidx.compose.ui.graphics.Color 8 | import com.joaomanaia.game2048.core.presentation.theme.WallpaperColorsDefaults.DEFAULT_HUE_SHIFT 9 | import com.joaomanaia.game2048.core.presentation.theme.WallpaperColorsDefaults.DEFAULT_LIGHTNESS_SHIFT 10 | import com.joaomanaia.game2048.core.presentation.theme.WallpaperColorsDefaults.DEFAULT_SATURATION_SHIFT 11 | import com.joaomanaia.game2048.model.toHsl 12 | 13 | actual class WallpaperColors( 14 | private val context: Context 15 | ) { 16 | actual fun isSupported(): Boolean { 17 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 18 | } 19 | 20 | actual fun generateWallpaperColors(): Set { 21 | if (!isSupported()) { 22 | return emptySet() 23 | } 24 | 25 | val primaryColor = generateWallpaperPrimaryColor() ?: return emptySet() 26 | 27 | return generateWallpaperVariations(primaryColor) 28 | } 29 | 30 | @RequiresApi(Build.VERSION_CODES.O_MR1) 31 | private fun generateWallpaperPrimaryColor(): Color? { 32 | val colors = WallpaperManager 33 | .getInstance(context) 34 | .getWallpaperColors(WallpaperManager.FLAG_SYSTEM) 35 | 36 | val primaryColorArgb = colors?.primaryColor?.toArgb() 37 | return primaryColorArgb?.let { Color(it) } 38 | } 39 | 40 | private fun generateWallpaperVariations(primaryColor: Color): Set { 41 | val positiveShiftVariations = primaryColor.generateVariations( 42 | hueShift = DEFAULT_HUE_SHIFT, 43 | saturationShift = DEFAULT_SATURATION_SHIFT, 44 | lightnessShift = DEFAULT_LIGHTNESS_SHIFT 45 | ) 46 | 47 | val negativeShiftVariations = primaryColor.generateVariations( 48 | hueShift = -DEFAULT_HUE_SHIFT, 49 | saturationShift = -DEFAULT_SATURATION_SHIFT, 50 | lightnessShift = -DEFAULT_LIGHTNESS_SHIFT 51 | ) 52 | 53 | return setOf(primaryColor, positiveShiftVariations, negativeShiftVariations) 54 | } 55 | 56 | private fun Color.generateVariations( 57 | hueShift: Int, 58 | saturationShift: Float, 59 | lightnessShift: Float 60 | ): Color { 61 | val hsl = toHsl() 62 | 63 | val hue = (hsl.hue + hueShift) % 360 64 | val saturation = hsl.saturation + saturationShift 65 | val lightness = hsl.lightness + lightnessShift 66 | 67 | return Color.hsl( 68 | hue = hue.coerceIn(0f, 360f), 69 | saturation = saturation.coerceIn(0f, 1f), 70 | lightness = lightness.coerceIn(0f, 1f) 71 | ) 72 | } 73 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/joaomanaia/game2048/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi 7 | import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass 8 | import androidx.compose.runtime.CompositionLocalProvider 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.setValue 12 | import androidx.compose.ui.input.key.KeyEvent 13 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 14 | import androidx.lifecycle.Lifecycle 15 | import androidx.lifecycle.lifecycleScope 16 | import androidx.lifecycle.repeatOnLifecycle 17 | import com.joaomanaia.game2048.core.compose.KeyEventHandler 18 | import com.joaomanaia.game2048.core.compose.LocalKeyEventHandlers 19 | import com.joaomanaia.game2048.presentation.App 20 | import com.joaomanaia.game2048.presentation.MainUiState 21 | import com.joaomanaia.game2048.presentation.MainViewModel 22 | import kotlinx.coroutines.flow.collect 23 | import kotlinx.coroutines.flow.onEach 24 | import kotlinx.coroutines.launch 25 | import org.koin.androidx.viewmodel.ext.android.viewModel 26 | 27 | @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) 28 | class MainActivity : ComponentActivity() { 29 | private val viewModel: MainViewModel by viewModel() 30 | 31 | private val keyEventHandlers = mutableListOf() 32 | 33 | override fun onCreate(savedInstanceState: Bundle?) { 34 | val splashScreen = installSplashScreen() 35 | super.onCreate(savedInstanceState) 36 | 37 | var uiState: MainUiState by mutableStateOf(MainUiState.Loading) 38 | 39 | lifecycleScope.launch { 40 | lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { 41 | viewModel.uiState 42 | .onEach { uiState = it } 43 | .collect() 44 | } 45 | } 46 | 47 | splashScreen.setKeepOnScreenCondition { 48 | when (uiState) { 49 | MainUiState.Loading -> true 50 | is MainUiState.Success -> false 51 | } 52 | } 53 | 54 | setContent { 55 | val windowSizeClass = calculateWindowSizeClass() 56 | 57 | CompositionLocalProvider( 58 | LocalKeyEventHandlers provides keyEventHandlers 59 | ) { 60 | App( 61 | windowSizeClass = windowSizeClass, 62 | uiState = uiState 63 | ) 64 | } 65 | } 66 | } 67 | 68 | override fun onKeyUp(keyCode: Int, event: android.view.KeyEvent): Boolean { 69 | return keyEventHandlers.reversed().any { 70 | it(keyCode, KeyEvent(event)) 71 | } || super.onKeyUp(keyCode, event) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi 2 | import androidx.compose.material3.windowsizeclass.WindowSizeClass 3 | import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass 4 | import androidx.compose.runtime.CompositionLocalProvider 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.mutableStateListOf 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.runtime.rememberCoroutineScope 10 | import androidx.compose.runtime.setValue 11 | import androidx.compose.ui.input.key.key 12 | import androidx.compose.ui.input.key.nativeKeyCode 13 | import androidx.compose.ui.window.Window 14 | import androidx.compose.ui.window.application 15 | import androidx.compose.ui.window.rememberWindowState 16 | import com.joaomanaia.game2048.core.compose.KeyEventHandler 17 | import com.joaomanaia.game2048.core.compose.LocalKeyEventHandlers 18 | import com.joaomanaia.game2048.di.KoinStarter 19 | import com.joaomanaia.game2048.presentation.App 20 | import com.joaomanaia.game2048.presentation.MainUiState 21 | import com.joaomanaia.game2048.presentation.MainViewModel 22 | import kotlinx.coroutines.launch 23 | import org.koin.compose.koinInject 24 | import org.koin.logger.SLF4JLogger 25 | 26 | @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) 27 | fun main() = application { 28 | System.setProperty(org.slf4j.simple.SimpleLogger.DEFAULT_LOG_LEVEL_KEY, "TRACE") 29 | 30 | KoinStarter.init { 31 | logger(SLF4JLogger()) 32 | } 33 | 34 | val mainViewModel: MainViewModel = koinInject() 35 | 36 | var uiState: MainUiState by remember { mutableStateOf(MainUiState.Loading) } 37 | 38 | // with(LocalLifecycleOwner.current) { 39 | // lifecycleScope.launch { 40 | // lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { 41 | // mainViewModel.uiState 42 | // .onEach { uiState = it } 43 | // .collect() 44 | // } 45 | // } 46 | // } 47 | 48 | val scope = rememberCoroutineScope() 49 | scope.launch { 50 | mainViewModel.uiState.collect { uiState = it } 51 | } 52 | 53 | val windowState = rememberWindowState() 54 | 55 | val keyEventHandlers = remember { mutableStateListOf() } 56 | 57 | Window( 58 | state = windowState, 59 | onCloseRequest = ::exitApplication, 60 | title = "Game 2048", 61 | onKeyEvent = { event -> 62 | keyEventHandlers.reversed().any { 63 | it(event.key.nativeKeyCode, event) 64 | } 65 | } 66 | ) { 67 | // val windowSizeClass = WindowSizeClass.calculateFromSize(windowState.size) 68 | val windowSizeClass = calculateWindowSizeClass() 69 | 70 | CompositionLocalProvider( 71 | LocalKeyEventHandlers provides keyEventHandlers 72 | ) { 73 | App( 74 | windowSizeClass = windowSizeClass, 75 | uiState = uiState 76 | ) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/presentation/App.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.presentation 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.Surface 7 | import androidx.compose.material3.windowsizeclass.WindowSizeClass 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.navigation.compose.NavHost 12 | import androidx.navigation.compose.composable 13 | import androidx.navigation.compose.rememberNavController 14 | import com.joaomanaia.game2048.core.navigation.Screen 15 | import com.joaomanaia.game2048.core.presentation.theme.Game2048Theme 16 | import com.joaomanaia.game2048.presentation.color_settings.ColorSettingsScreen 17 | import com.joaomanaia.game2048.presentation.game.GameScreen 18 | import org.koin.compose.KoinContext 19 | 20 | @Composable 21 | internal fun App( 22 | windowSizeClass: WindowSizeClass, 23 | uiState: MainUiState, 24 | ) { 25 | val darkTheme = shouldUseDarkTheme(uiState) 26 | 27 | Game2048Theme( 28 | seedColor = getSeedColor(uiState), 29 | useDarkTheme = darkTheme, 30 | amoledMode = getAmoledMode(uiState) 31 | ) { 32 | KoinContext { 33 | // A surface container using the 'background' color from the theme 34 | Surface( 35 | modifier = Modifier.fillMaxSize(), 36 | color = MaterialTheme.colorScheme.background 37 | ) { 38 | val navController = rememberNavController() 39 | 40 | NavHost( 41 | navController = navController, 42 | startDestination = Screen.GAME.name 43 | ) { 44 | composable(Screen.GAME.name) { 45 | GameScreen( 46 | windowSizeClass = windowSizeClass, 47 | navController = navController 48 | ) 49 | } 50 | 51 | composable(Screen.COLOR_SETTINGS.name) { 52 | ColorSettingsScreen(navController = navController) 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | /** 61 | * Returns `true` if dark theme should be used, as a function of the [uiState] and the 62 | * current system context. 63 | */ 64 | @Composable 65 | private fun shouldUseDarkTheme( 66 | uiState: MainUiState, 67 | ): Boolean = when (uiState) { 68 | MainUiState.Loading -> isSystemInDarkTheme() 69 | is MainUiState.Success -> uiState.darkThemeConfig.shouldUseDarkTheme() 70 | } 71 | 72 | private fun getSeedColor(uiState: MainUiState): Color? { 73 | return when (uiState) { 74 | MainUiState.Loading -> null 75 | is MainUiState.Success -> uiState.seedColor 76 | } 77 | } 78 | 79 | private fun getAmoledMode(uiState: MainUiState): Boolean { 80 | return when (uiState) { 81 | MainUiState.Loading -> false 82 | is MainUiState.Success -> uiState.amoledMode 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/core/presentation/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.presentation.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val md_theme_light_primary = Color(0xFF745b00) 6 | val md_theme_light_onPrimary = Color(0xFFffffff) 7 | val md_theme_light_primaryContainer = Color(0xFFffe07f) 8 | val md_theme_light_onPrimaryContainer = Color(0xFF241a00) 9 | val md_theme_light_secondary = Color(0xFF695e3f) 10 | val md_theme_light_onSecondary = Color(0xFFffffff) 11 | val md_theme_light_secondaryContainer = Color(0xFFf1e1bb) 12 | val md_theme_light_onSecondaryContainer = Color(0xFF221b04) 13 | val md_theme_light_tertiary = Color(0xFF47664b) 14 | val md_theme_light_onTertiary = Color(0xFFffffff) 15 | val md_theme_light_tertiaryContainer = Color(0xFFc8ebc9) 16 | val md_theme_light_onTertiaryContainer = Color(0xFF03210c) 17 | val md_theme_light_error = Color(0xFFba1b1b) 18 | val md_theme_light_errorContainer = Color(0xFFffdad4) 19 | val md_theme_light_onError = Color(0xFFffffff) 20 | val md_theme_light_onErrorContainer = Color(0xFF410001) 21 | val md_theme_light_background = Color(0xFFfffbf7) 22 | val md_theme_light_onBackground = Color(0xFF1e1b16) 23 | val md_theme_light_surface = Color(0xFFfffbf7) 24 | val md_theme_light_onSurface = Color(0xFF1e1b16) 25 | val md_theme_light_surfaceVariant = Color(0xFFebe1cf) 26 | val md_theme_light_onSurfaceVariant = Color(0xFF4c4639) 27 | val md_theme_light_outline = Color(0xFF7d7667) 28 | val md_theme_light_inverseOnSurface = Color(0xFFf7f0e7) 29 | val md_theme_light_inverseSurface = Color(0xFF33302a) 30 | val md_theme_light_inversePrimary = Color(0xFFedc22e) 31 | val md_theme_light_shadow = Color(0xFF000000) 32 | 33 | val md_theme_dark_primary = Color(0xFFedc22e) 34 | val md_theme_dark_onPrimary = Color(0xFF3d2f00) 35 | val md_theme_dark_primaryContainer = Color(0xFF574400) 36 | val md_theme_dark_onPrimaryContainer = Color(0xFFffe07f) 37 | val md_theme_dark_secondary = Color(0xFFd4c5a0) 38 | val md_theme_dark_onSecondary = Color(0xFF383016) 39 | val md_theme_dark_secondaryContainer = Color(0xFF50462a) 40 | val md_theme_dark_onSecondaryContainer = Color(0xFFf1e1bb) 41 | val md_theme_dark_tertiary = Color(0xFFaccfae) 42 | val md_theme_dark_onTertiary = Color(0xFF18371f) 43 | val md_theme_dark_tertiaryContainer = Color(0xFF2f4d34) 44 | val md_theme_dark_onTertiaryContainer = Color(0xFFc8ebc9) 45 | val md_theme_dark_error = Color(0xFFffb4a9) 46 | val md_theme_dark_errorContainer = Color(0xFF930006) 47 | val md_theme_dark_onError = Color(0xFF680003) 48 | val md_theme_dark_onErrorContainer = Color(0xFFffdad4) 49 | val md_theme_dark_background = Color(0xFF1e1b16) 50 | val md_theme_dark_onBackground = Color(0xFFe9e2d9) 51 | val md_theme_dark_surface = Color(0xFF1e1b16) 52 | val md_theme_dark_onSurface = Color(0xFFe9e2d9) 53 | val md_theme_dark_surfaceVariant = Color(0xFF4c4639) 54 | val md_theme_dark_onSurfaceVariant = Color(0xFFcfc6b4) 55 | val md_theme_dark_outline = Color(0xFF989080) 56 | val md_theme_dark_inverseOnSurface = Color(0xFF1e1b16) 57 | val md_theme_dark_inverseSurface = Color(0xFFe9e2d9) 58 | val md_theme_dark_inversePrimary = Color(0xFF745b00) 59 | val md_theme_dark_shadow = Color(0xFF000000) 60 | 61 | val seed = Color(0xFFedc22e) 62 | val error = Color(0xFFba1b1b) 63 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/presentation/game/components/grid/GameGrid.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.presentation.game.components.grid 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.layout.size 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.key 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.draw.clip 11 | import androidx.compose.ui.geometry.Offset 12 | import androidx.compose.ui.text.TextStyle 13 | import com.joaomanaia.game2048.core.presentation.theme.TileColorsGenerator 14 | import com.joaomanaia.game2048.core.presentation.theme.spacing 15 | import com.joaomanaia.game2048.model.GridTileMovement 16 | 17 | @Composable 18 | internal fun GameGrid( 19 | modifier: Modifier = Modifier, 20 | gridTileMovements: List, 21 | moveCount: Int, 22 | gridSize: Int, 23 | hueParams: TileColorsGenerator.HueParams 24 | ) { 25 | GridContainer( 26 | modifier = modifier 27 | .clip(MaterialTheme.shapes.extraLarge) 28 | .background(MaterialTheme.colorScheme.surfaceContainer) 29 | .padding(MaterialTheme.spacing.medium) 30 | .clip(MaterialTheme.shapes.large) 31 | .drawBackgroundGrid( 32 | gridSize = gridSize, 33 | emptyTileColor = MaterialTheme.colorScheme.surfaceVariant 34 | ), 35 | gridSize = gridSize 36 | ) { tileSize, tileOffsetPx, textFontSize -> 37 | for (gridTileMovement in gridTileMovements) { 38 | // Each grid tile is laid out at (0,0) in the box. Shifting tiles are then translated 39 | // to their correct position in the grid, and added tiles are scaled from 0 to 1. 40 | val (fromGridTile, toGridTile) = gridTileMovement 41 | val fromScale = if (fromGridTile == null) 0f else 1f 42 | val toOffset = Offset( 43 | x = toGridTile.cell.col * tileOffsetPx, 44 | y = toGridTile.cell.row * tileOffsetPx 45 | ) 46 | val fromOffset = fromGridTile?.let { 47 | Offset(x = it.cell.col * tileOffsetPx, y = it.cell.row * tileOffsetPx) 48 | } ?: toOffset 49 | 50 | // In 2048, tiles are frequently being removed and added to the grid. As a result, 51 | // the order in which grid tiles are rendered is constantly changing after each 52 | // recomposition. In order to ensure that each tile animates from its correct 53 | // starting position, it is critical that we assign each tile a unique ID using 54 | // the key() function. 55 | key(toGridTile.tile.id) { 56 | GridTileText( 57 | modifier = Modifier.size(tileSize), 58 | tile = toGridTile.tile, 59 | fromScale = fromScale, 60 | fromOffset = fromOffset, 61 | toOffset = toOffset, 62 | moveCount = moveCount, 63 | textStyle = TextStyle(fontSize = textFontSize), 64 | hueParams = hueParams 65 | ) 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/data/repository/SaveGameRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.data.repository 2 | 3 | import com.joaomanaia.game2048.core.common.preferences.GameDataPreferencesCommon 4 | import com.joaomanaia.game2048.core.datastore.manager.DataStoreManager 5 | import com.joaomanaia.game2048.domain.repository.SaveGameRepository 6 | import com.joaomanaia.game2048.model.Cell 7 | import com.joaomanaia.game2048.model.Grid 8 | import com.joaomanaia.game2048.model.GridTile 9 | import com.joaomanaia.game2048.model.GridTileMovement 10 | import com.joaomanaia.game2048.model.Tile 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.serialization.encodeToString 13 | import kotlinx.serialization.json.Json 14 | 15 | class SaveGameRepositoryImpl( 16 | private val gameDataStoreManager: DataStoreManager 17 | ) : SaveGameRepository { 18 | override suspend fun checkSaveGameExists() = getSavedGrid().isNotEmpty() 19 | 20 | override suspend fun getSavedGrid(): Grid { 21 | val savedGridStr = gameDataStoreManager.getPreference(GameDataPreferencesCommon.Grid) 22 | return Json.decodeFromString(savedGridStr) 23 | } 24 | 25 | override suspend fun getSavedGridTileMovements(): List { 26 | return getSavedGrid() 27 | .flatMapIndexed { row, tiles -> 28 | tiles.mapIndexed { col, tile -> 29 | if (tile == null) null else GridTileMovement.add( 30 | gridTile = GridTile(cell = Cell(row, col), tile = tile) 31 | ) 32 | } 33 | }.filterNotNull() 34 | .also { movements -> 35 | // Reset the tile id counter 36 | Tile.tileIdCounter = movements.maxOf { it.toGridTile.tile.id } 37 | } 38 | } 39 | 40 | override suspend fun getSavedCurrentScore(): Int = 41 | gameDataStoreManager.getPreference(GameDataPreferencesCommon.CurrentScore) 42 | 43 | override suspend fun getSavedBestScore(): Int = 44 | gameDataStoreManager.getPreference(GameDataPreferencesCommon.BestScore) 45 | 46 | override suspend fun getGridSize(): Int = 47 | gameDataStoreManager.getPreference(GameDataPreferencesCommon.GridSize) 48 | 49 | override fun getGridSizeFlow(): Flow = 50 | gameDataStoreManager.getPreferenceFlow(GameDataPreferencesCommon.GridSize) 51 | 52 | override suspend fun updateGridSize(newSize: Int) { 53 | gameDataStoreManager.editPreference( 54 | key = GameDataPreferencesCommon.GridSize.key, 55 | newValue = newSize 56 | ) 57 | } 58 | 59 | override suspend fun saveGame(grid: Grid, currentScore: Int) { 60 | val gridStr = Json.encodeToString(grid) 61 | 62 | gameDataStoreManager.editPreferences( 63 | GameDataPreferencesCommon.Grid.key to gridStr, 64 | GameDataPreferencesCommon.CurrentScore.key to currentScore 65 | ) 66 | } 67 | 68 | override suspend fun saveGame( 69 | grid: Grid, 70 | currentScore: Int, 71 | bestScore: Int 72 | ) { 73 | val gridStr = Json.encodeToString(grid) 74 | 75 | gameDataStoreManager.editPreferences( 76 | GameDataPreferencesCommon.Grid.key to gridStr, 77 | GameDataPreferencesCommon.CurrentScore.key to currentScore, 78 | GameDataPreferencesCommon.BestScore.key to bestScore 79 | ) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/core/presentation/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.presentation.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material3.darkColorScheme 5 | import androidx.compose.material3.lightColorScheme 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.graphics.Color 8 | 9 | @Composable 10 | expect fun Game2048Theme( 11 | seedColor: Color? = null, 12 | useDarkTheme: Boolean = isSystemInDarkTheme(), 13 | amoledMode: Boolean = false, 14 | isDynamic: Boolean = true, 15 | content: @Composable () -> Unit 16 | ) 17 | 18 | internal val LightThemeColors = lightColorScheme( 19 | primary = md_theme_light_primary, 20 | onPrimary = md_theme_light_onPrimary, 21 | primaryContainer = md_theme_light_primaryContainer, 22 | onPrimaryContainer = md_theme_light_onPrimaryContainer, 23 | secondary = md_theme_light_secondary, 24 | onSecondary = md_theme_light_onSecondary, 25 | secondaryContainer = md_theme_light_secondaryContainer, 26 | onSecondaryContainer = md_theme_light_onSecondaryContainer, 27 | tertiary = md_theme_light_tertiary, 28 | onTertiary = md_theme_light_onTertiary, 29 | tertiaryContainer = md_theme_light_tertiaryContainer, 30 | onTertiaryContainer = md_theme_light_onTertiaryContainer, 31 | error = md_theme_light_error, 32 | errorContainer = md_theme_light_errorContainer, 33 | onError = md_theme_light_onError, 34 | onErrorContainer = md_theme_light_onErrorContainer, 35 | background = md_theme_light_background, 36 | onBackground = md_theme_light_onBackground, 37 | surface = md_theme_light_surface, 38 | onSurface = md_theme_light_onSurface, 39 | surfaceVariant = md_theme_light_surfaceVariant, 40 | onSurfaceVariant = md_theme_light_onSurfaceVariant, 41 | outline = md_theme_light_outline, 42 | inverseOnSurface = md_theme_light_inverseOnSurface, 43 | inverseSurface = md_theme_light_inverseSurface, 44 | inversePrimary = md_theme_light_inversePrimary, 45 | ) 46 | 47 | internal val DarkThemeColors = darkColorScheme( 48 | primary = md_theme_dark_primary, 49 | onPrimary = md_theme_dark_onPrimary, 50 | primaryContainer = md_theme_dark_primaryContainer, 51 | onPrimaryContainer = md_theme_dark_onPrimaryContainer, 52 | secondary = md_theme_dark_secondary, 53 | onSecondary = md_theme_dark_onSecondary, 54 | secondaryContainer = md_theme_dark_secondaryContainer, 55 | onSecondaryContainer = md_theme_dark_onSecondaryContainer, 56 | tertiary = md_theme_dark_tertiary, 57 | onTertiary = md_theme_dark_onTertiary, 58 | tertiaryContainer = md_theme_dark_tertiaryContainer, 59 | onTertiaryContainer = md_theme_dark_onTertiaryContainer, 60 | error = md_theme_dark_error, 61 | errorContainer = md_theme_dark_errorContainer, 62 | onError = md_theme_dark_onError, 63 | onErrorContainer = md_theme_dark_onErrorContainer, 64 | background = md_theme_dark_background, 65 | onBackground = md_theme_dark_onBackground, 66 | surface = md_theme_dark_surface, 67 | onSurface = md_theme_dark_onSurface, 68 | surfaceVariant = md_theme_dark_surfaceVariant, 69 | onSurfaceVariant = md_theme_dark_onSurfaceVariant, 70 | outline = md_theme_dark_outline, 71 | inverseOnSurface = md_theme_dark_inverseOnSurface, 72 | inverseSurface = md_theme_dark_inverseSurface, 73 | inversePrimary = md_theme_dark_inversePrimary, 74 | ) 75 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #745B00 3 | #FFFFFF 4 | #FFE07F 5 | #241A00 6 | #695E3F 7 | #FFFFFF 8 | #F1E1BB 9 | #221B04 10 | #47664B 11 | #FFFFFF 12 | #C8EBC9 13 | #03210C 14 | #BA1B1B 15 | #FFDAD4 16 | #FFFFFF 17 | #410001 18 | #FFFBF7 19 | #1E1B16 20 | #FFFBF7 21 | #1E1B16 22 | #EBE1CF 23 | #4C4639 24 | #7D7667 25 | #F7F0E7 26 | #33302A 27 | #EDC22E 28 | #000000 29 | #EDC22E 30 | #EDC22E 31 | #3D2F00 32 | #574400 33 | #FFE07F 34 | #D4C5A0 35 | #383016 36 | #50462A 37 | #F1E1BB 38 | #ACCFAE 39 | #18371F 40 | #2F4D34 41 | #C8EBC9 42 | #FFB4A9 43 | #930006 44 | #680003 45 | #FFDAD4 46 | #1E1B16 47 | #E9E2D9 48 | #1E1B16 49 | #E9E2D9 50 | #4C4639 51 | #CFC6B4 52 | #989080 53 | #1E1B16 54 | #E9E2D9 55 | #745B00 56 | #000000 57 | #745B00 58 | -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/kotlin/com/joaomanaia/game2048/core/datastore/manager/DataStoreManagerImpl.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.datastore.manager 2 | 3 | import com.joaomanaia.game2048.core.common.preferences.GameDataPreferencesCommon 4 | import com.joaomanaia.game2048.core.datastore.MutablePreferences 5 | import com.joaomanaia.game2048.core.datastore.Preferences 6 | import com.joaomanaia.game2048.core.datastore.PreferencesKey 7 | import com.joaomanaia.game2048.core.datastore.PreferencesPair 8 | import com.joaomanaia.game2048.core.datastore.edit 9 | import io.github.oshai.kotlinlogging.KotlinLogging 10 | import kotlinx.browser.localStorage 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.coroutines.flow.MutableStateFlow 13 | import kotlinx.coroutines.flow.asSharedFlow 14 | import kotlinx.coroutines.flow.distinctUntilChanged 15 | import kotlinx.coroutines.flow.firstOrNull 16 | import kotlinx.coroutines.flow.map 17 | 18 | private val logger = KotlinLogging.logger("DataStoreManagerImpl") 19 | 20 | actual class DataStoreManagerImpl : DataStoreManager { 21 | private val mutablePreferences = MutableStateFlow( 22 | MutablePreferences(preferencesMap = loadFromLocalStorage()) 23 | ) 24 | override val preferenceFlow: Flow = mutablePreferences.asSharedFlow() 25 | 26 | private fun loadFromLocalStorage(): MutableMap, Any> { 27 | val allPrefs = GameDataPreferencesCommon.allPreferences() 28 | val loadedPrefs = mutableMapOf, Any>() 29 | 30 | for (i in 0 until localStorage.length) { 31 | val keyStr = localStorage.key(i) ?: continue 32 | val req = allPrefs.find { it.key.name == keyStr } ?: continue 33 | 34 | val value = getPreferenceValueFromLocalStorage(req) 35 | loadedPrefs[req.key] = value 36 | } 37 | 38 | logger.debug { "Loaded ${loadedPrefs.size} preferences from localStorage" } 39 | 40 | return loadedPrefs 41 | } 42 | 43 | private fun getPreferenceValueFromLocalStorage(req: PreferenceRequest): T { 44 | val valueStr = localStorage.getItem(req.key.name) ?: return req.defaultValue 45 | 46 | return when (req.defaultValue) { 47 | is String -> valueStr 48 | is Boolean -> valueStr.toBoolean() 49 | is Int -> valueStr.toInt() 50 | is Float -> valueStr.toFloat() 51 | else -> throw IllegalArgumentException("Unsupported type: ${req.defaultValue!!::class}") 52 | } as T 53 | } 54 | 55 | override suspend fun getPreference(preferenceEntry: PreferenceRequest): T { 56 | return preferenceFlow 57 | .firstOrNull() 58 | ?.get(preferenceEntry.key) ?: preferenceEntry.defaultValue 59 | } 60 | 61 | override fun getPreferenceFlow(request: PreferenceRequest): Flow = preferenceFlow.map { 62 | it[request.key] ?: request.defaultValue 63 | }.distinctUntilChanged() 64 | 65 | override suspend fun editPreference(key: PreferencesKey, newValue: T) { 66 | saveToLocalStorage(key, newValue) 67 | mutablePreferences.edit { preferences -> preferences[key] = newValue } 68 | } 69 | 70 | override suspend fun editPreferences(vararg prefs: PreferencesPair<*>) { 71 | mutablePreferences.edit { preferences -> 72 | prefs.forEach { pref -> 73 | saveToLocalStorage(pref.key, pref.value) 74 | preferences.plusAssign(pref) 75 | } 76 | } 77 | } 78 | 79 | private fun saveToLocalStorage(key: PreferencesKey<*>, value: Any?) { 80 | when (value) { 81 | is String -> localStorage.setItem(key.name, value) 82 | is Boolean -> localStorage.setItem(key.name, value.toString()) 83 | is Int -> localStorage.setItem(key.name, value.toString()) 84 | is Float -> localStorage.setItem(key.name, value.toString()) 85 | else -> throw IllegalArgumentException("Unsupported type: $value") 86 | } 87 | } 88 | 89 | override suspend fun clearPreferences() { 90 | mutablePreferences.edit { preferences -> preferences.clear() } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi 2 | import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.CompositionLocalProvider 5 | import androidx.compose.runtime.DisposableEffect 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.mutableStateListOf 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.runtime.rememberCoroutineScope 11 | import androidx.compose.runtime.setValue 12 | import androidx.compose.ui.ExperimentalComposeUiApi 13 | import androidx.compose.ui.InternalComposeUiApi 14 | import androidx.compose.ui.input.key.Key 15 | import androidx.compose.ui.input.key.KeyEvent 16 | import androidx.compose.ui.input.key.KeyEventType 17 | import androidx.compose.ui.window.CanvasBasedWindow 18 | import com.joaomanaia.game2048.core.compose.KeyEventHandler 19 | import com.joaomanaia.game2048.core.compose.LocalKeyEventHandlers 20 | import com.joaomanaia.game2048.di.KoinStarter 21 | import com.joaomanaia.game2048.presentation.App 22 | import com.joaomanaia.game2048.presentation.MainUiState 23 | import com.joaomanaia.game2048.presentation.MainViewModel 24 | import io.github.oshai.kotlinlogging.KotlinLogging 25 | import io.github.oshai.kotlinlogging.KotlinLoggingConfiguration 26 | import io.github.oshai.kotlinlogging.Level 27 | import kotlinx.browser.window 28 | import kotlinx.coroutines.launch 29 | import org.koin.compose.koinInject 30 | import org.w3c.dom.events.Event 31 | import org.w3c.dom.events.KeyboardEvent 32 | 33 | private val logger = KotlinLogging.logger("WasmJsMain") 34 | 35 | @OptIn( 36 | ExperimentalComposeUiApi::class, 37 | ExperimentalMaterial3WindowSizeClassApi::class, 38 | InternalComposeUiApi::class 39 | ) 40 | fun main() { 41 | KotlinLoggingConfiguration.logLevel = Level.DEBUG 42 | KoinStarter.init() 43 | 44 | CanvasBasedWindow(canvasElementId = "ComposeTarget") { 45 | val mainViewModel: MainViewModel = koinInject() 46 | 47 | var uiState: MainUiState by remember { mutableStateOf(MainUiState.Loading) } 48 | 49 | // with(LocalLifecycleOwner.current) { 50 | // lifecycleScope.launch { 51 | // lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { 52 | // mainViewModel.uiState 53 | // .onEach { uiState = it } 54 | // .collect() 55 | // } 56 | // } 57 | // } 58 | 59 | val scope = rememberCoroutineScope() 60 | scope.launch { 61 | mainViewModel.uiState.collect { uiState = it } 62 | } 63 | 64 | val keyEventHandlers = remember { mutableStateListOf() } 65 | DetectKeyEvents { nativeEvent -> 66 | val keyEvent = KeyEvent( 67 | key = Key(nativeEvent.keyCode.toLong()), 68 | type = KeyEventType.KeyUp, 69 | ) 70 | 71 | keyEventHandlers.reversed().any { 72 | it(nativeEvent.keyCode, keyEvent) 73 | } 74 | } 75 | 76 | // val windowSizeClass = WindowSizeClass.calculateFromSize(windowState.size) 77 | val windowSizeClass = calculateWindowSizeClass() 78 | 79 | CompositionLocalProvider( 80 | LocalKeyEventHandlers provides keyEventHandlers 81 | ) { 82 | App( 83 | windowSizeClass = windowSizeClass, 84 | uiState = uiState 85 | ) 86 | } 87 | } 88 | } 89 | 90 | @Composable 91 | private fun DetectKeyEvents( 92 | onKeyEvent: (nativeEvent: KeyboardEvent) -> Boolean 93 | ) { 94 | DisposableEffect(Unit) { 95 | val handleKeyDown: (Event) -> Unit = { event: Event -> 96 | if (event is KeyboardEvent) { 97 | logger.debug { "Keyboard event, key: ${event.key}" } 98 | onKeyEvent(event) 99 | } 100 | } 101 | 102 | window.addEventListener("keyup", handleKeyDown) 103 | 104 | onDispose { window.removeEventListener("keyup", handleKeyDown) } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /composeApp/compose-desktop.pro: -------------------------------------------------------------------------------- 1 | # Ktor 2 | -keep class io.ktor.** { *; } 3 | -keepclassmembers class io.ktor.** { volatile ; } 4 | -keep class io.ktor.client.engine.cio.** { *; } 5 | -keep class kotlinx.coroutines.** { *; } 6 | -dontwarn kotlinx.atomicfu.** 7 | -dontwarn io.netty.** 8 | -dontwarn com.typesafe.** 9 | -dontwarn org.slf4j.** 10 | -keep class org.slf4j.**{ *; } 11 | -keep class com.sun.jna.* { *; } 12 | -keep class * implements com.sun.jna.* { *; } 13 | 14 | # Obfuscation breaks coroutines/ktor for some reason 15 | # -dontobfuscate 16 | 17 | -dontwarn kotlinx.coroutines.debug.* 18 | 19 | -keep class kotlin.** { *; } 20 | -keep class kotlinx.** { *; } 21 | -keep class kotlinx.coroutines.** { *; } 22 | -keep class org.jetbrains.skia.** { *; } 23 | -keep class org.jetbrains.skiko.** { *; } 24 | 25 | -assumenosideeffects public class androidx.compose.runtime.ComposerKt { 26 | void sourceInformation(androidx.compose.runtime.Composer,java.lang.String); 27 | void sourceInformationMarkerStart(androidx.compose.runtime.Composer,int,java.lang.String); 28 | void sourceInformationMarkerEnd(androidx.compose.runtime.Composer); 29 | } 30 | 31 | # Keep `Companion` object fields of serializable classes. 32 | # This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects. 33 | -if @kotlinx.serialization.Serializable class ** 34 | -keepclassmembers class <1> { 35 | static <1>$Companion Companion; 36 | } 37 | 38 | # Keep `serializer()` on companion objects (both default and named) of serializable classes. 39 | -if @kotlinx.serialization.Serializable class ** { 40 | static **$* *; 41 | } 42 | -keepclassmembers class <2>$<3> { 43 | kotlinx.serialization.KSerializer serializer(...); 44 | } 45 | 46 | # Keep `INSTANCE.serializer()` of serializable objects. 47 | -if @kotlinx.serialization.Serializable class ** { 48 | public static ** INSTANCE; 49 | } 50 | -keepclassmembers class <1> { 51 | public static <1> INSTANCE; 52 | kotlinx.serialization.KSerializer serializer(...); 53 | } 54 | 55 | # @Serializable and @Polymorphic are used at runtime for polymorphic serialization. 56 | -keepattributes RuntimeVisibleAnnotations,AnnotationDefault 57 | 58 | -keepattributes *Annotation*, InnerClasses 59 | -dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations 60 | -dontnote kotlinx.serialization.SerializationKt 61 | 62 | # Keep Serializers 63 | 64 | -keep,includedescriptorclasses class com.company.package.**$$serializer { *; } # <-- Change com.company.package 65 | -keepclassmembers class com.company.package.** { # <-- Change com.company.package to yours 66 | *** Companion; 67 | } 68 | -keepclasseswithmembers class com.company.package.** { # <-- Change com.company.package to yours 69 | kotlinx.serialization.KSerializer serializer(...); 70 | } 71 | 72 | # When kotlinx.serialization.json.JsonObjectSerializer occurs 73 | 74 | -keepclassmembers class kotlinx.serialization.json.** { 75 | *** Companion; 76 | } 77 | -keepclasseswithmembers class kotlinx.serialization.json.** { 78 | kotlinx.serialization.KSerializer serializer(...); 79 | } 80 | 81 | # JSR 305 annotations are for embedding nullability information. 82 | -dontwarn javax.annotation.** 83 | 84 | # A resource is loaded with a relative path so the package of this class must be preserved. 85 | -adaptresourcefilenames okhttp3/internal/publicsuffix/PublicSuffixDatabase.gz 86 | 87 | # Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. 88 | -dontwarn org.codehaus.mojo.animal_sniffer.* 89 | 90 | # OkHttp platform used only on JVM and when Conscrypt and other security providers are available. 91 | -dontwarn okhttp3.internal.platform.** 92 | -dontwarn org.conscrypt.** 93 | -dontwarn org.bouncycastle.** 94 | -dontwarn org.openjsse.** 95 | #################################### SLF4J ##################################### 96 | -dontwarn org.slf4j.** 97 | 98 | # Prevent runtime crashes from use of class.java.getName() 99 | -dontwarn javax.naming.** 100 | 101 | -dontwarn androidx.** 102 | 103 | -dontwarn io.github.oshai.kotlinlogging.coroutines.KotlinLoggingAsyncMDCKt 104 | -dontwarn okio.** 105 | 106 | # Ignore warnings and Don't obfuscate for now 107 | -dontobfuscate 108 | -ignorewarnings 109 | -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/kotlin/com/joaomanaia/game2048/core/datastore/PreferencesKey.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.datastore 2 | 3 | import kotlinx.coroutines.flow.MutableStateFlow 4 | import kotlinx.coroutines.flow.updateAndGet 5 | 6 | actual abstract class Preferences internal actual constructor() { 7 | actual abstract fun asMap(): Map, Any> 8 | 9 | actual abstract operator fun contains(key: PreferencesKey): Boolean 10 | 11 | actual abstract operator fun get(key: PreferencesKey): T? 12 | 13 | actual fun toMutablePreferences(): MutablePreferences { 14 | return MutablePreferences(asMap().toMutableMap(), startFrozen = false) 15 | } 16 | 17 | actual fun toPreferences(): Preferences { 18 | return MutablePreferences(asMap().toMutableMap(), startFrozen = true) 19 | } 20 | } 21 | 22 | actual class PreferencesKey internal actual constructor( 23 | actual val name: String 24 | ) { 25 | actual infix fun to(value: T): PreferencesPair = PreferencesPair(this, value) 26 | 27 | actual override fun equals(other: Any?): Boolean = 28 | if (other is PreferencesKey<*>) { 29 | name == other.name 30 | } else { 31 | false 32 | } 33 | 34 | actual override fun hashCode(): Int { 35 | return name.hashCode() 36 | } 37 | 38 | actual override fun toString(): String = name 39 | } 40 | 41 | actual class PreferencesPair internal actual constructor( 42 | actual val key: PreferencesKey, 43 | actual val value: T 44 | ) 45 | 46 | actual class MutablePreferences internal constructor( 47 | internal val preferencesMap: MutableMap, Any> = mutableMapOf(), 48 | startFrozen: Boolean = true 49 | ) : Preferences() { 50 | override fun asMap(): Map, Any> { 51 | return immutableMap( 52 | preferencesMap.entries.associate { entry -> 53 | when (val value = entry.value) { 54 | is ByteArray -> Pair(entry.key, value.copyOf()) 55 | else -> Pair(entry.key, entry.value) 56 | } 57 | } 58 | ) 59 | } 60 | 61 | override fun contains(key: PreferencesKey): Boolean { 62 | return preferencesMap.containsKey(key) 63 | } 64 | 65 | override fun get(key: PreferencesKey): T? { 66 | @Suppress("UNCHECKED_CAST") 67 | return when (val value = preferencesMap[key]) { 68 | is ByteArray -> value.copyOf() 69 | else -> value 70 | } 71 | as T? 72 | } 73 | 74 | operator fun set(key: PreferencesKey, value: T) { 75 | setUnchecked(key, value) 76 | } 77 | 78 | internal fun setUnchecked(key: PreferencesKey<*>, value: Any?) { 79 | when (value) { 80 | null -> remove(key) 81 | // Copy set so changes to input don't change Preferences. Wrap in unmodifiableSet so 82 | // returned instances can't be changed. 83 | is Set<*> -> preferencesMap[key] = immutableCopyOfSet(value) 84 | is ByteArray -> preferencesMap[key] = value.copyOf() 85 | else -> preferencesMap[key] = value 86 | } 87 | } 88 | 89 | fun remove(key: PreferencesKey): T { 90 | return preferencesMap.remove(key) as T 91 | } 92 | 93 | operator fun plusAssign(pair: PreferencesPair<*>) { 94 | putAll(pair) 95 | } 96 | 97 | fun putAll(vararg pairs: PreferencesPair<*>) { 98 | pairs.forEach { setUnchecked(it.key, it.value) } 99 | } 100 | 101 | fun clear() { 102 | preferencesMap.clear() 103 | } 104 | } 105 | 106 | suspend fun MutableStateFlow.edit( 107 | transform: suspend (MutablePreferences) -> Unit 108 | ): Preferences { 109 | return this.updateAndGet { 110 | // It's safe to return MutablePreferences since we freeze it in 111 | // PreferencesDataStore.updateData() 112 | it.toMutablePreferences().apply { transform(this) } 113 | } 114 | } 115 | 116 | private fun immutableMap(map: Map): Map { 117 | // TODO:(b/239829063) Find a replacement for java's unmodifyable map. For now just make a copy. 118 | return map.toMap() 119 | } 120 | 121 | private fun immutableCopyOfSet(set: Set): Set = set.toSet() 122 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/presentation/game/components/grid/ChangeGameGridDialog.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.presentation.game.components.grid 2 | 3 | import androidx.compose.foundation.layout.Row 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.lazy.LazyColumn 7 | import androidx.compose.foundation.lazy.items 8 | import androidx.compose.foundation.selection.selectable 9 | import androidx.compose.foundation.selection.selectableGroup 10 | import androidx.compose.material3.AlertDialog 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.RadioButton 13 | import androidx.compose.material3.Text 14 | import androidx.compose.material3.TextButton 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.mutableStateOf 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.window.DialogProperties 21 | import com.joaomanaia.game2048.core.presentation.theme.spacing 22 | import game2048.composeapp.generated.resources.Res 23 | import game2048.composeapp.generated.resources.cancel 24 | import game2048.composeapp.generated.resources.grid_size 25 | import game2048.composeapp.generated.resources.grid_size_n 26 | import game2048.composeapp.generated.resources.ok 27 | import org.jetbrains.compose.resources.stringResource 28 | 29 | @Composable 30 | internal fun ChangeGameGridDialog( 31 | currentSize: Int, 32 | onDismissRequest: () -> Unit, 33 | onGridSizeChange: (size: Int) -> Unit 34 | ) { 35 | ChangeGameGridDialogImpl( 36 | sizes = listOf(3, 4, 5, 6, 7), 37 | currentSize = currentSize, 38 | onDismissRequest = onDismissRequest, 39 | onGridSizeChange = onGridSizeChange 40 | ) 41 | } 42 | 43 | @Composable 44 | private fun ChangeGameGridDialogImpl( 45 | sizes: List, 46 | currentSize: Int, 47 | onDismissRequest: () -> Unit, 48 | onGridSizeChange: (size: Int) -> Unit 49 | ) { 50 | val (selectedValue, changeSelectedValue) = remember { 51 | mutableStateOf(currentSize) 52 | } 53 | 54 | AlertDialog( 55 | onDismissRequest = onDismissRequest, 56 | title = { Text(text = stringResource(Res.string.grid_size)) }, 57 | text = { 58 | LazyColumn(modifier = Modifier.selectableGroup()) { 59 | items(items = sizes) { size -> 60 | val isSelected = selectedValue == size 61 | val onSelected = { changeSelectedValue(size) } 62 | Row( 63 | modifier = Modifier 64 | .fillMaxWidth() 65 | .selectable( 66 | selected = isSelected, 67 | onClick = { if (!isSelected) onSelected() } 68 | ) 69 | .padding(MaterialTheme.spacing.small), 70 | verticalAlignment = Alignment.CenterVertically 71 | ) { 72 | RadioButton( 73 | selected = isSelected, 74 | onClick = { if (!isSelected) onSelected() }, 75 | ) 76 | Text( 77 | text = stringResource(Res.string.grid_size_n, size), 78 | style = MaterialTheme.typography.bodyLarge, 79 | modifier = Modifier.padding(start = MaterialTheme.spacing.medium) 80 | ) 81 | } 82 | } 83 | } 84 | }, 85 | properties = DialogProperties( 86 | usePlatformDefaultWidth = true 87 | ), 88 | confirmButton = { 89 | TextButton( 90 | onClick = { 91 | onGridSizeChange(selectedValue) 92 | onDismissRequest() 93 | } 94 | ) { 95 | Text(text = stringResource(Res.string.ok)) 96 | } 97 | }, 98 | dismissButton = { 99 | TextButton(onClick = onDismissRequest) { 100 | Text(text = stringResource(Res.string.cancel)) 101 | } 102 | } 103 | ) 104 | } 105 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/presentation/color_settings/components/SelectablePaletteItem.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.presentation.color_settings.components 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.fadeIn 5 | import androidx.compose.animation.fadeOut 6 | import androidx.compose.animation.scaleIn 7 | import androidx.compose.animation.scaleOut 8 | import androidx.compose.foundation.background 9 | import androidx.compose.foundation.isSystemInDarkTheme 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.shape.CircleShape 14 | import androidx.compose.material.icons.Icons 15 | import androidx.compose.material.icons.rounded.Check 16 | import androidx.compose.material3.ColorScheme 17 | import androidx.compose.material3.Icon 18 | import androidx.compose.material3.MaterialTheme 19 | import androidx.compose.material3.Surface 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.draw.drawBehind 24 | import androidx.compose.ui.graphics.Color 25 | import androidx.compose.ui.unit.dp 26 | import com.joaomanaia.game2048.core.presentation.theme.spacing 27 | import com.materialkolor.rememberDynamicColorScheme 28 | 29 | @Composable 30 | internal fun SelectablePaletteItem( 31 | modifier: Modifier = Modifier, 32 | baseColor: Color, 33 | useDarkTheme: Boolean = isSystemInDarkTheme(), 34 | selected: Boolean = false, 35 | onClick: () -> Unit 36 | ) { 37 | val colorScheme = rememberDynamicColorScheme( 38 | seedColor = baseColor, 39 | isDark = useDarkTheme 40 | ) 41 | 42 | SelectablePaletteItem( 43 | modifier = modifier, 44 | colorScheme = colorScheme, 45 | selected = selected, 46 | onClick = onClick 47 | ) 48 | } 49 | 50 | @Composable 51 | private fun SelectablePaletteItem( 52 | modifier: Modifier = Modifier, 53 | colorScheme: ColorScheme, 54 | selected: Boolean = false, 55 | onClick: () -> Unit 56 | ) { 57 | Surface( 58 | modifier = modifier, 59 | selected = selected, 60 | onClick = onClick, 61 | shape = MaterialTheme.shapes.medium, 62 | tonalElevation = 8.dp 63 | ) { 64 | Box( 65 | contentAlignment = Alignment.Center, 66 | modifier = Modifier 67 | .padding(MaterialTheme.spacing.small) 68 | .drawPalette(colorPrimary = colorScheme.primary) 69 | ) { 70 | AnimatedVisibility( 71 | visible = selected, 72 | enter = fadeIn() + scaleIn(), 73 | exit = fadeOut() + scaleOut() 74 | ) { 75 | Icon( 76 | imageVector = Icons.Rounded.Check, 77 | contentDescription = "Selected", 78 | tint = colorScheme.onPrimaryContainer, 79 | modifier = Modifier 80 | .background( 81 | color = colorScheme.primaryContainer, 82 | shape = CircleShape 83 | ) 84 | .padding(SELECTED_CIRCLE_ICON_PADDING) 85 | ) 86 | } 87 | } 88 | } 89 | } 90 | 91 | /** 92 | * Draw a palette with 1 circle, representing the primary color. 93 | */ 94 | private fun Modifier.drawPalette( 95 | colorPrimary: Color, 96 | ): Modifier = drawBehind { 97 | // Draw the top half circle 98 | drawArc( 99 | color = colorPrimary, 100 | startAngle = 0f, 101 | sweepAngle = 360f, 102 | useCenter = true, 103 | ) 104 | } 105 | 106 | /** 107 | * Draw a palette with 3 circles, representing the primary, secondary, and tertiary colors. 108 | */ 109 | private fun Modifier.drawPalette( 110 | colorPrimary: Color, 111 | colorSecondary: Color, 112 | colorTertiary: Color, 113 | ): Modifier = drawBehind { 114 | // Draw the top half circle 115 | drawArc( 116 | color = colorPrimary, 117 | startAngle = 180f, 118 | sweepAngle = 180f, 119 | useCenter = true, 120 | ) 121 | 122 | // Draw the bottom left quarter circle 123 | drawArc( 124 | color = colorSecondary, 125 | startAngle = 90f, 126 | sweepAngle = 90f, 127 | useCenter = true, 128 | ) 129 | 130 | // Draw the bottom right quarter circle 131 | drawArc( 132 | color = colorTertiary, 133 | startAngle = 0f, 134 | sweepAngle = 90f, 135 | useCenter = true, 136 | ) 137 | } 138 | 139 | private val SELECTED_CIRCLE_ICON_PADDING = 6.dp 140 | -------------------------------------------------------------------------------- /composeApp/src/commonTest/kotlin/com/joaomanaia/game2048/core/util/GameUtilTest.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.util 2 | 3 | import assertk.assertAll 4 | import assertk.assertThat 5 | import assertk.assertions.isEqualTo 6 | import assertk.assertions.isFalse 7 | import assertk.assertions.isIn 8 | import assertk.assertions.isNotNull 9 | import assertk.assertions.isNull 10 | import assertk.assertions.isTrue 11 | import com.joaomanaia.game2048.model.Cell 12 | import com.joaomanaia.game2048.model.Grid 13 | import com.joaomanaia.game2048.model.GridTile 14 | import com.joaomanaia.game2048.model.GridTileMovement 15 | import com.joaomanaia.game2048.model.Tile 16 | import kotlin.test.Test 17 | 18 | internal class GameUtilTest { 19 | @Test 20 | fun emptyGrid_shouldReturn_emptyGrid() { 21 | val grid = emptyGrid(4) 22 | val expectedGrid: Grid = listOf( 23 | listOf(null, null, null, null), 24 | listOf(null, null, null, null), 25 | listOf(null, null, null, null), 26 | listOf(null, null, null, null) 27 | ) 28 | 29 | assertThat(grid).isEqualTo(expectedGrid) 30 | } 31 | 32 | @Test 33 | fun createRandomAddedTile_returnsNull_whenGridIsFull() { 34 | val grid = listOf( 35 | listOf(Tile(2), Tile(2), Tile(2), Tile(2)), 36 | listOf(Tile(2), Tile(2), Tile(2), Tile(2)), 37 | listOf(Tile(2), Tile(2), Tile(2), Tile(2)), 38 | listOf(Tile(2), Tile(2), Tile(2), Tile(2)) 39 | ) 40 | val result = createRandomAddedTile(grid) 41 | assertThat(result).isNull() 42 | } 43 | 44 | @Test 45 | fun createRandomAddedTile_returnsAGridTileMovement_whenGridHasEmptyCells() { 46 | val grid = listOf( 47 | listOf(Tile(2), Tile(2), Tile(2), Tile(2)), 48 | listOf(Tile(2), null, Tile(2), Tile(2)), 49 | listOf(Tile(2), Tile(2), Tile(2), Tile(2)), 50 | listOf(Tile(2), Tile(2), Tile(2), Tile(2)) 51 | ) 52 | val result = createRandomAddedTile(grid) 53 | 54 | assertAll { 55 | assertThat(result, "result").isNotNull() 56 | assertThat(result?.fromGridTile, "fromGridTile").isNull() 57 | 58 | assertThat(result?.toGridTile, "toGridTile").isNotNull() 59 | assertThat(result?.toGridTile?.tile?.num, "num").isIn(2, 4) 60 | assertThat(result?.toGridTile?.cell, "cell").isEqualTo(Cell(1, 1)) 61 | } 62 | } 63 | 64 | @Test 65 | fun test_hasGridChanged() { 66 | var movements = listOf() 67 | 68 | // Grid has not changed when the grid is empty 69 | assertThat(hasGridChanged(movements)).isFalse() 70 | 71 | val tile1 = GridTile(Cell(0, 0), Tile(2)) 72 | val tile2 = GridTile(Cell(2, 2), Tile(4)) 73 | 74 | // Grid has changed when a cell is added 75 | movements = listOf( 76 | GridTileMovement(fromGridTile = null, toGridTile = tile1), 77 | GridTileMovement(fromGridTile = null, toGridTile = tile2), 78 | ) 79 | assertThat(hasGridChanged(movements)).isTrue() 80 | 81 | // Grid has changed when no cells are changed or added 82 | movements = listOf( 83 | GridTileMovement(fromGridTile = tile1, toGridTile = tile1), 84 | GridTileMovement(fromGridTile = tile2, toGridTile = tile2) 85 | ) 86 | assertThat(hasGridChanged(movements)).isFalse() 87 | 88 | // Grid has changed when a tile position is changed 89 | movements = listOf( 90 | GridTileMovement( 91 | fromGridTile = tile1, 92 | toGridTile = tile1.copy(Cell(1, 1)) 93 | ), 94 | GridTileMovement(fromGridTile = tile2, toGridTile = tile2) 95 | ) 96 | assertThat(hasGridChanged(movements)).isTrue() 97 | } 98 | 99 | @Test 100 | fun testGameOver_noMovesPossible_returnsTrue() { 101 | val grid: Grid = listOf( 102 | listOf(Tile(2), Tile(4), Tile(8)), 103 | listOf(Tile(16), Tile(32), Tile(64)), 104 | listOf(Tile(128), Tile(256), Tile(512)) 105 | ) 106 | 107 | val isGameOver = checkIsGameOver(grid) 108 | assertThat(isGameOver).isTrue() 109 | } 110 | 111 | @Test 112 | fun testGameOver_emptyGrid_returnsFalse() { 113 | val isGameOver = checkIsGameOver(emptyGrid(3)) 114 | assertThat(isGameOver).isFalse() 115 | } 116 | 117 | @Test 118 | fun testGameOver_withPossibleMoves_returnsFalse() { 119 | val grid: Grid = listOf( 120 | listOf(Tile(2), null, null), 121 | listOf(Tile(4), null, null), 122 | listOf(Tile(8), null, null) 123 | ) 124 | 125 | val isGameOver = checkIsGameOver(grid) 126 | assertThat(isGameOver).isFalse() 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/presentation/game/components/grid/GridTileText.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.presentation.game.components.grid 2 | 3 | import androidx.compose.animation.core.Animatable 4 | import androidx.compose.animation.core.Spring 5 | import androidx.compose.animation.core.VectorConverter 6 | import androidx.compose.animation.core.spring 7 | import androidx.compose.animation.core.tween 8 | import androidx.compose.foundation.layout.wrapContentSize 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.LaunchedEffect 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.drawBehind 16 | import androidx.compose.ui.geometry.CornerRadius 17 | import androidx.compose.ui.geometry.Offset 18 | import androidx.compose.ui.graphics.Color 19 | import androidx.compose.ui.graphics.graphicsLayer 20 | import androidx.compose.ui.text.TextStyle 21 | import androidx.compose.ui.text.style.TextAlign 22 | import androidx.compose.ui.unit.dp 23 | import com.joaomanaia.game2048.core.presentation.theme.TileColorsGenerator 24 | import com.joaomanaia.game2048.model.Tile 25 | import kotlinx.coroutines.launch 26 | 27 | @Composable 28 | internal fun GridTileText( 29 | modifier: Modifier = Modifier, 30 | tile: Tile, 31 | tileBaseColor: Color = MaterialTheme.colorScheme.primary, 32 | fromScale: Float, 33 | fromOffset: Offset, 34 | toOffset: Offset, 35 | moveCount: Int, 36 | textStyle: TextStyle, 37 | hueParams: TileColorsGenerator.HueParams = TileColorsGenerator.HueParams() 38 | ) { 39 | val containerColor = remember(tile, tileBaseColor, hueParams) { 40 | TileColorsGenerator.getColorForTile( 41 | tile = tile, 42 | baseColor = tileBaseColor, 43 | hueParams = hueParams 44 | ) 45 | } 46 | 47 | GridTileText( 48 | modifier = modifier, 49 | num = tile.num, 50 | fromScale = fromScale, 51 | fromOffset = fromOffset, 52 | toOffset = toOffset, 53 | moveCount = moveCount, 54 | containerColor = containerColor, 55 | contentColor = Color.White, 56 | textStyle = textStyle 57 | ) 58 | } 59 | 60 | @Composable 61 | internal fun GridTileText( 62 | modifier: Modifier = Modifier, 63 | num: Int, 64 | fromScale: Float, 65 | fromOffset: Offset, 66 | toOffset: Offset, 67 | moveCount: Int, 68 | textStyle: TextStyle, 69 | containerColor: Color, 70 | contentColor: Color 71 | ) { 72 | val animatedScale = remember { Animatable(fromScale) } 73 | val animatedOffset = remember { Animatable(fromOffset, Offset.VectorConverter) } 74 | 75 | LaunchedEffect(key1 = moveCount) { 76 | launch { 77 | animatedScale.snapTo(if (moveCount == 0) 1f else fromScale) 78 | animatedScale.animateTo( 79 | targetValue = 1f, 80 | animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy) 81 | ) 82 | } 83 | launch { 84 | animatedOffset.animateTo(toOffset, tween(durationMillis = 200)) 85 | } 86 | } 87 | 88 | GridTileText( 89 | modifier = modifier.graphicsLayer( 90 | scaleX = animatedScale.value, 91 | scaleY = animatedScale.value, 92 | translationX = animatedOffset.value.x, 93 | translationY = animatedOffset.value.y, 94 | ), 95 | num = num, 96 | textStyle = textStyle, 97 | containerColor = containerColor, 98 | contentColor = contentColor, 99 | ) 100 | } 101 | 102 | @Composable 103 | internal fun GridTileText( 104 | modifier: Modifier = Modifier, 105 | num: Int, 106 | textStyle: TextStyle, 107 | containerColor: Color, 108 | contentColor: Color 109 | ) { 110 | val formattedText = remember(num) { formatNumber(num) } 111 | 112 | Text( 113 | text = formattedText, 114 | modifier = modifier 115 | .drawBehind { 116 | val radius = GRID_TILE_RADIUS.toPx() 117 | 118 | drawRoundRect( 119 | color = containerColor, 120 | cornerRadius = CornerRadius(radius, radius) 121 | ) 122 | } 123 | .wrapContentSize(), 124 | color = contentColor, 125 | textAlign = TextAlign.Center, 126 | maxLines = 1, 127 | style = textStyle 128 | ) 129 | } 130 | 131 | private fun formatNumber(number: Int): String { 132 | return when { 133 | number >= 1_000_000 -> "${number / 1_000_000}M" 134 | number >= 10_000 -> "${number / 1_000}k" 135 | else -> number.toString() 136 | } 137 | } 138 | 139 | private val GRID_TILE_RADIUS = 4.dp 140 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/presentation/color_settings/components/BaseColorChooser.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.presentation.color_settings.components 2 | 3 | import androidx.compose.animation.animateColorAsState 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.RowScope 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.foundation.lazy.LazyRow 12 | import androidx.compose.foundation.lazy.items 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.Surface 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.getValue 18 | import androidx.compose.runtime.mutableStateOf 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.runtime.setValue 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.graphics.Color 23 | import androidx.compose.ui.text.style.TextAlign 24 | import androidx.compose.ui.unit.dp 25 | import com.joaomanaia.game2048.core.presentation.theme.spacing 26 | 27 | private val defaultBasicColors = setOf(Color.Blue, Color.Red, Color.Green, Color.Yellow) 28 | 29 | @Composable 30 | internal fun BaseColorChooser( 31 | modifier: Modifier = Modifier, 32 | currentSeedColor: Color?, 33 | useDarkTheme: Boolean, 34 | wallpaperColors: Set, 35 | defaultSelectedTab: PreviewTabButtonType = PreviewTabButtonType.BACKGROUND_COLOR, 36 | onColorSelected: (Color) -> Unit 37 | ) { 38 | var selectedTab by remember { mutableStateOf(defaultSelectedTab) } 39 | 40 | val colorsToSelect: Set = remember(selectedTab, wallpaperColors) { 41 | if (selectedTab == PreviewTabButtonType.BACKGROUND_COLOR && wallpaperColors.isNotEmpty()) { 42 | wallpaperColors 43 | } else { 44 | defaultBasicColors 45 | } 46 | } 47 | 48 | Column( 49 | modifier = modifier, 50 | verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.medium) 51 | ) { 52 | if (wallpaperColors.isNotEmpty()) { 53 | Row( 54 | modifier = Modifier.fillMaxWidth(), 55 | horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.small) 56 | ) { 57 | PreviewTabButton( 58 | title = "Background color", 59 | selected = selectedTab == PreviewTabButtonType.BACKGROUND_COLOR, 60 | onClick = { selectedTab = PreviewTabButtonType.BACKGROUND_COLOR } 61 | ) 62 | PreviewTabButton( 63 | title = "Basic color", 64 | selected = selectedTab == PreviewTabButtonType.BASIC_COLOR, 65 | onClick = { selectedTab = PreviewTabButtonType.BASIC_COLOR } 66 | ) 67 | } 68 | } 69 | 70 | LazyRow( 71 | modifier = Modifier.fillMaxWidth(), 72 | horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.small) 73 | ) { 74 | items(items = colorsToSelect.toList()) { color -> 75 | SelectablePaletteItem( 76 | modifier = Modifier.size(75.dp), 77 | baseColor = color, 78 | useDarkTheme = useDarkTheme, 79 | selected = currentSeedColor == color, 80 | onClick = { onColorSelected(color) } 81 | ) 82 | } 83 | } 84 | } 85 | } 86 | 87 | internal enum class PreviewTabButtonType { 88 | BACKGROUND_COLOR, 89 | BASIC_COLOR 90 | } 91 | 92 | @Composable 93 | private fun RowScope.PreviewTabButton( 94 | modifier: Modifier = Modifier, 95 | title: String, 96 | selected: Boolean, 97 | onClick: () -> Unit 98 | ) { 99 | val containerColor = animateColorAsState( 100 | targetValue = if (selected) { 101 | MaterialTheme.colorScheme.primary 102 | } else { 103 | MaterialTheme.colorScheme.surface 104 | }, 105 | label = "Container Color" 106 | ) 107 | 108 | Surface( 109 | modifier = modifier.weight(1f), 110 | tonalElevation = 4.dp, 111 | shape = MaterialTheme.shapes.large, 112 | onClick = onClick, 113 | selected = selected, 114 | color = containerColor.value, 115 | ) { 116 | Text( 117 | text = title, 118 | modifier = Modifier 119 | .fillMaxWidth() 120 | .padding(MaterialTheme.spacing.medium), 121 | style = MaterialTheme.typography.titleSmall, 122 | textAlign = TextAlign.Center 123 | ) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/presentation/color_settings/components/DarkThemeDialogPicker.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.presentation.color_settings.components 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material3.ListItem 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.RadioButton 10 | import androidx.compose.material3.Surface 11 | import androidx.compose.material3.Switch 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.clip 16 | import androidx.compose.ui.semantics.Role 17 | import androidx.compose.ui.window.Dialog 18 | import com.joaomanaia.game2048.core.presentation.theme.DarkThemeConfig 19 | import com.joaomanaia.game2048.core.presentation.theme.spacing 20 | 21 | @Composable 22 | internal fun DarkThemeDialogPicker( 23 | useDarkTheme: Boolean, 24 | amoledMode: Boolean, 25 | darkThemeConfig: DarkThemeConfig, 26 | onDarkThemeChanged: (DarkThemeConfig) -> Unit, 27 | onAmoledModeChanged: (Boolean) -> Unit, 28 | onDismissRequest: () -> Unit 29 | ) { 30 | Dialog( 31 | onDismissRequest = onDismissRequest 32 | ) { 33 | Surface( 34 | shape = MaterialTheme.shapes.extraLarge 35 | ) { 36 | val spaceMedium = MaterialTheme.spacing.medium 37 | 38 | Column( 39 | modifier = Modifier.padding(spaceMedium) 40 | ) { 41 | Text( 42 | text = "Dark theme", 43 | style = MaterialTheme.typography.titleLarge, 44 | modifier = Modifier.padding(spaceMedium) 45 | ) 46 | DarkThemeConfig.entries.forEach { item -> 47 | ThemeRadioButton( 48 | darkThemeConfig = item, 49 | selected = item == darkThemeConfig, 50 | onClick = { onDarkThemeChanged(item) } 51 | ) 52 | } 53 | 54 | AnimatedVisibility(visible = useDarkTheme) { 55 | Column { 56 | Text( 57 | text = "Other", 58 | style = MaterialTheme.typography.labelLarge, 59 | modifier = Modifier.padding(start = spaceMedium, top = spaceMedium), 60 | color = MaterialTheme.colorScheme.primary 61 | ) 62 | 63 | Surface( 64 | modifier = Modifier.padding(top = spaceMedium), 65 | shape = MaterialTheme.shapes.large, 66 | onClick = { onAmoledModeChanged(!amoledMode) } 67 | ) { 68 | ListItem( 69 | headlineContent = { 70 | Text(text = "AMOLED Mode") 71 | }, 72 | trailingContent = { 73 | Switch( 74 | checked = amoledMode, 75 | onCheckedChange = onAmoledModeChanged 76 | ) 77 | } 78 | ) 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | 87 | @Composable 88 | private fun ThemeRadioButton( 89 | modifier: Modifier = Modifier, 90 | darkThemeConfig: DarkThemeConfig, 91 | selected: Boolean, 92 | onClick: () -> Unit 93 | ) { 94 | ThemeRadioButton( 95 | modifier = modifier, 96 | title = darkThemeConfig.getRadioButtonTitle(), 97 | selected = selected, 98 | onClick = onClick 99 | ) 100 | } 101 | 102 | @Composable 103 | private fun ThemeRadioButton( 104 | modifier: Modifier = Modifier, 105 | title: String, 106 | selected: Boolean, 107 | onClick: () -> Unit 108 | ) { 109 | ListItem( 110 | modifier = modifier 111 | .clip(MaterialTheme.shapes.large) 112 | .clickable( 113 | onClick = onClick, 114 | role = Role.RadioButton 115 | ), 116 | headlineContent = { Text(text = title) }, 117 | trailingContent = { 118 | RadioButton( 119 | selected = selected, 120 | onClick = onClick 121 | ) 122 | } 123 | ) 124 | } 125 | 126 | private fun DarkThemeConfig.getRadioButtonTitle(): String { 127 | return when (this) { 128 | DarkThemeConfig.FOLLOW_SYSTEM -> "Follow System" 129 | DarkThemeConfig.LIGHT -> "Disabled" 130 | DarkThemeConfig.DARK -> "Enabled" 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/presentation/game/components/icons/Grid4x4.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.presentation.game.components.icons 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.materialIcon 5 | import androidx.compose.material.icons.materialPath 6 | import androidx.compose.ui.graphics.vector.ImageVector 7 | 8 | public val Icons.Rounded.Grid4x4: ImageVector 9 | get() { 10 | if (_grid4x4 != null) { 11 | return _grid4x4!! 12 | } 13 | _grid4x4 = materialIcon(name = "Rounded.Grid4x4") { 14 | materialPath { 15 | moveTo(22.0f, 6.0f) 16 | lineTo(22.0f, 6.0f) 17 | curveToRelative(0.0f, -0.55f, -0.45f, -1.0f, -1.0f, -1.0f) 18 | horizontalLineToRelative(-2.0f) 19 | verticalLineTo(3.0f) 20 | curveToRelative(0.0f, -0.55f, -0.45f, -1.0f, -1.0f, -1.0f) 21 | horizontalLineToRelative(0.0f) 22 | curveToRelative(-0.55f, 0.0f, -1.0f, 0.45f, -1.0f, 1.0f) 23 | verticalLineToRelative(2.0f) 24 | horizontalLineToRelative(-4.0f) 25 | verticalLineTo(3.0f) 26 | curveToRelative(0.0f, -0.55f, -0.45f, -1.0f, -1.0f, -1.0f) 27 | horizontalLineToRelative(0.0f) 28 | curveToRelative(-0.55f, 0.0f, -1.0f, 0.45f, -1.0f, 1.0f) 29 | verticalLineToRelative(2.0f) 30 | horizontalLineTo(7.0f) 31 | verticalLineTo(3.0f) 32 | curveToRelative(0.0f, -0.55f, -0.45f, -1.0f, -1.0f, -1.0f) 33 | horizontalLineToRelative(0.0f) 34 | curveTo(5.45f, 2.0f, 5.0f, 2.45f, 5.0f, 3.0f) 35 | verticalLineToRelative(2.0f) 36 | horizontalLineTo(3.0f) 37 | curveTo(2.45f, 5.0f, 2.0f, 5.45f, 2.0f, 6.0f) 38 | verticalLineToRelative(0.0f) 39 | curveToRelative(0.0f, 0.55f, 0.45f, 1.0f, 1.0f, 1.0f) 40 | horizontalLineToRelative(2.0f) 41 | verticalLineToRelative(4.0f) 42 | horizontalLineTo(3.0f) 43 | curveToRelative(-0.55f, 0.0f, -1.0f, 0.45f, -1.0f, 1.0f) 44 | verticalLineToRelative(0.0f) 45 | curveToRelative(0.0f, 0.55f, 0.45f, 1.0f, 1.0f, 1.0f) 46 | horizontalLineToRelative(2.0f) 47 | verticalLineToRelative(4.0f) 48 | horizontalLineTo(3.0f) 49 | curveToRelative(-0.55f, 0.0f, -1.0f, 0.45f, -1.0f, 1.0f) 50 | verticalLineToRelative(0.0f) 51 | curveToRelative(0.0f, 0.55f, 0.45f, 1.0f, 1.0f, 1.0f) 52 | horizontalLineToRelative(2.0f) 53 | verticalLineToRelative(2.0f) 54 | curveToRelative(0.0f, 0.55f, 0.45f, 1.0f, 1.0f, 1.0f) 55 | horizontalLineToRelative(0.0f) 56 | curveToRelative(0.55f, 0.0f, 1.0f, -0.45f, 1.0f, -1.0f) 57 | verticalLineToRelative(-2.0f) 58 | horizontalLineToRelative(4.0f) 59 | verticalLineToRelative(2.0f) 60 | curveToRelative(0.0f, 0.55f, 0.45f, 1.0f, 1.0f, 1.0f) 61 | horizontalLineToRelative(0.0f) 62 | curveToRelative(0.55f, 0.0f, 1.0f, -0.45f, 1.0f, -1.0f) 63 | verticalLineToRelative(-2.0f) 64 | horizontalLineToRelative(4.0f) 65 | verticalLineToRelative(2.0f) 66 | curveToRelative(0.0f, 0.55f, 0.45f, 1.0f, 1.0f, 1.0f) 67 | horizontalLineToRelative(0.0f) 68 | curveToRelative(0.55f, 0.0f, 1.0f, -0.45f, 1.0f, -1.0f) 69 | verticalLineToRelative(-2.0f) 70 | horizontalLineToRelative(2.0f) 71 | curveToRelative(0.55f, 0.0f, 1.0f, -0.45f, 1.0f, -1.0f) 72 | verticalLineToRelative(0.0f) 73 | curveToRelative(0.0f, -0.55f, -0.45f, -1.0f, -1.0f, -1.0f) 74 | horizontalLineToRelative(-2.0f) 75 | verticalLineToRelative(-4.0f) 76 | horizontalLineToRelative(2.0f) 77 | curveToRelative(0.55f, 0.0f, 1.0f, -0.45f, 1.0f, -1.0f) 78 | verticalLineToRelative(0.0f) 79 | curveToRelative(0.0f, -0.55f, -0.45f, -1.0f, -1.0f, -1.0f) 80 | horizontalLineToRelative(-2.0f) 81 | verticalLineTo(7.0f) 82 | horizontalLineToRelative(2.0f) 83 | curveTo(21.55f, 7.0f, 22.0f, 6.55f, 22.0f, 6.0f) 84 | close() 85 | moveTo(7.0f, 7.0f) 86 | horizontalLineToRelative(4.0f) 87 | verticalLineToRelative(4.0f) 88 | horizontalLineTo(7.0f) 89 | verticalLineTo(7.0f) 90 | close() 91 | moveTo(7.0f, 17.0f) 92 | verticalLineToRelative(-4.0f) 93 | horizontalLineToRelative(4.0f) 94 | verticalLineToRelative(4.0f) 95 | horizontalLineTo(7.0f) 96 | close() 97 | moveTo(17.0f, 17.0f) 98 | horizontalLineToRelative(-4.0f) 99 | verticalLineToRelative(-4.0f) 100 | horizontalLineToRelative(4.0f) 101 | verticalLineTo(17.0f) 102 | close() 103 | moveTo(17.0f, 11.0f) 104 | horizontalLineToRelative(-4.0f) 105 | verticalLineTo(7.0f) 106 | horizontalLineToRelative(4.0f) 107 | verticalLineTo(11.0f) 108 | close() 109 | } 110 | } 111 | return _grid4x4!! 112 | } 113 | 114 | private var _grid4x4: ImageVector? = null -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/joaomanaia/game2048/presentation/color_settings/ColorSettingsScreenViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.presentation.color_settings 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.toArgb 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.joaomanaia.game2048.core.presentation.theme.WallpaperColors 8 | import com.joaomanaia.game2048.core.common.preferences.GameDataPreferencesCommon 9 | import com.joaomanaia.game2048.core.datastore.manager.DataStoreManager 10 | import com.joaomanaia.game2048.core.presentation.theme.DarkThemeConfig 11 | import com.joaomanaia.game2048.domain.usecase.GetHueParamsUseCase 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.MutableStateFlow 14 | import kotlinx.coroutines.flow.SharingStarted 15 | import kotlinx.coroutines.flow.combine 16 | import kotlinx.coroutines.flow.map 17 | import kotlinx.coroutines.flow.stateIn 18 | import kotlinx.coroutines.flow.update 19 | import kotlinx.coroutines.launch 20 | 21 | class ColorSettingsScreenViewModel( 22 | private val gameDataStoreManager: DataStoreManager, 23 | wallpaperColors: WallpaperColors, 24 | getHueParamsUseCase: GetHueParamsUseCase 25 | ) : ViewModel() { 26 | private val _uiState = MutableStateFlow(ColorSettingsUiState()) 27 | val uiState = combine( 28 | _uiState, 29 | getDarkThemeConfig(), 30 | gameDataStoreManager.getPreferenceFlow(GameDataPreferencesCommon.AmoledMode), 31 | getSeedColorFlow(), 32 | getHueParamsUseCase() 33 | ) { state, darkThemeConfig, amoledMode, seedColor, hueParams -> 34 | state.copy( 35 | darkThemeConfig = darkThemeConfig, 36 | amoledMode = amoledMode, 37 | seedColor = seedColor, 38 | hueParams = hueParams 39 | ) 40 | }.stateIn( 41 | scope = viewModelScope, 42 | started = SharingStarted.WhileSubscribed(5000), 43 | initialValue = ColorSettingsUiState() 44 | ) 45 | 46 | init { 47 | wallpaperColors 48 | .generateWallpaperColors() 49 | .also { colors -> 50 | _uiState.update { currentState -> 51 | currentState.copy(wallpaperColors = colors) 52 | } 53 | } 54 | } 55 | 56 | private fun getDarkThemeConfig() = gameDataStoreManager 57 | .getPreferenceFlow(GameDataPreferencesCommon.DarkThemeConfig) 58 | .map(DarkThemeConfig::valueOf) 59 | 60 | private fun getSeedColorFlow(): Flow = gameDataStoreManager 61 | .getPreferenceFlow(GameDataPreferencesCommon.SeedColor) 62 | .map { colorArgb -> 63 | if (colorArgb != -1) Color(colorArgb) else null 64 | } 65 | 66 | fun onEvent(event: ColorSettingsUiEvent) { 67 | when (event) { 68 | is ColorSettingsUiEvent.OnDarkThemeChanged -> updateDarkThemeConfig(event.config) 69 | is ColorSettingsUiEvent.OnAmoledModeChanged -> updateAmoledMode(event.amoledMode) 70 | is ColorSettingsUiEvent.OnSeedColorChanged -> updateSeedColor(event.color) 71 | is ColorSettingsUiEvent.OnIncrementHueChanged -> updateIncrementHue(event.increment) 72 | is ColorSettingsUiEvent.OnHueIncrementChanged -> updateHueIncrement(event.increment) 73 | is ColorSettingsUiEvent.OnHueSaturationChanged -> updateHueSaturation(event.saturation) 74 | is ColorSettingsUiEvent.OnHueLightnessChanged -> updateHueLightness(event.lightness) 75 | } 76 | } 77 | 78 | private fun updateDarkThemeConfig(config: DarkThemeConfig) { 79 | viewModelScope.launch { 80 | gameDataStoreManager.editPreference( 81 | key = GameDataPreferencesCommon.DarkThemeConfig.key, 82 | newValue = config.name 83 | ) 84 | } 85 | } 86 | 87 | private fun updateAmoledMode(amoledMode: Boolean) { 88 | viewModelScope.launch { 89 | gameDataStoreManager.editPreference( 90 | key = GameDataPreferencesCommon.AmoledMode.key, 91 | newValue = amoledMode 92 | ) 93 | } 94 | } 95 | 96 | private fun updateSeedColor(color: Color) { 97 | viewModelScope.launch { 98 | gameDataStoreManager.editPreference( 99 | key = GameDataPreferencesCommon.SeedColor.key, 100 | newValue = color.toArgb() 101 | ) 102 | } 103 | } 104 | 105 | private fun updateIncrementHue(increment: Boolean) { 106 | viewModelScope.launch { 107 | gameDataStoreManager.editPreference( 108 | key = GameDataPreferencesCommon.IncrementHue.key, 109 | newValue = increment 110 | ) 111 | } 112 | } 113 | 114 | private fun updateHueIncrement(incrementValue: Float) { 115 | viewModelScope.launch { 116 | gameDataStoreManager.editPreference( 117 | key = GameDataPreferencesCommon.HueIncrementValue.key, 118 | newValue = incrementValue 119 | ) 120 | } 121 | } 122 | 123 | private fun updateHueSaturation(saturation: Float) { 124 | viewModelScope.launch { 125 | gameDataStoreManager.editPreference( 126 | key = GameDataPreferencesCommon.HueSaturation.key, 127 | newValue = saturation 128 | ) 129 | } 130 | } 131 | 132 | private fun updateHueLightness(lightness: Float) { 133 | viewModelScope.launch { 134 | gameDataStoreManager.editPreference( 135 | key = GameDataPreferencesCommon.HueLightness.key, 136 | newValue = lightness 137 | ) 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | 3 | androidGradlePlugin = "8.5.0" 4 | androidxCoreKtx = "1.13.1" 5 | androidxDataStore = "1.1.1" 6 | androidxNavigation = "2.8.0-alpha02" 7 | androidxSplashscreen = "1.0.1" 8 | activityCompose = "1.9.0" 9 | assertk = "0.28.1" 10 | compose-plugin = "1.6.11" 11 | composeBom = "2024.06.00" 12 | constraintlayoutCompose = "0.4.0" 13 | lifecycleViewmodelCompose = "2.8.0-dev1698" 14 | kotlin = "2.0.0" 15 | kotlinxSerializationJson = "1.7.0" 16 | kotlinxCoroutines = "1.9.0-RC" 17 | koin-bom = "3.6.0-Beta4" 18 | koinComposeMultiplatform = "1.2.0-Beta4" 19 | ksp = "2.0.0-1.0.22" 20 | googleMaterial = "1.12.0" 21 | hilt = "2.51.1" 22 | hiltAndroidx = "1.2.0" 23 | junitJupiter = "5.10.2" 24 | materialKolor = "1.7.0" 25 | #windowSizeClass = "1.3.0-beta04" 26 | windowSizeClass = "0.5.0" 27 | kotlinLogging = "7.0.0" 28 | slf4j = "2.0.13" 29 | 30 | [libraries] 31 | 32 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCoreKtx" } 33 | androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidxSplashscreen" } 34 | androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "androidxDataStore" } 35 | androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleViewmodelCompose" } 36 | androidx-lifecycle-viewmodel-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" } 37 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } 38 | androidx-constraintlayout-compose = { module = "tech.annexflow.compose:constraintlayout-compose-multiplatform", version.ref = "constraintlayoutCompose" } 39 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } 40 | androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } 41 | androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } 42 | androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } 43 | androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } 44 | androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } 45 | androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } 46 | # TODO: change to official window size class when available in wasmJs 47 | #androidx-compose-material3-windowSizeClass = { group = "androidx.compose.material3", name = "material3-window-size-class", version.ref = "windowSizeClass" } 48 | androidx-compose-material3-windowSizeClass = { group = "dev.chrisbanes.material3", name = "material3-window-size-class-multiplatform", version.ref = "windowSizeClass" } 49 | androidx-compose-material-iconsExtended = { group = "androidx.compose.material", name = "material-icons-extended" } 50 | androidx-navigation-compose = { group = "org.jetbrains.androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" } 51 | assertk = { module = "com.willowtreeapps.assertk:assertk", version.ref = "assertk" } 52 | hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } 53 | hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } 54 | hilt-navigationCompose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltAndroidx" } 55 | kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } 56 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } 57 | kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } 58 | kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinxCoroutines" } 59 | kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } 60 | koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" } 61 | koin-android = { module = "io.insert-koin:koin-android" } 62 | koin-core = { module = "io.insert-koin:koin-core" } 63 | koin-logger-slf4j = { module = "io.insert-koin:koin-logger-slf4j" } 64 | koin-compose = { module = "io.insert-koin:koin-compose" } 65 | koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koinComposeMultiplatform" } 66 | google-material = { group = "com.google.android.material", name = "material", version.ref = "googleMaterial" } 67 | junit-jupiter-params = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "junitJupiter" } 68 | materialKolor = { module = "com.materialkolor:material-kolor", version.ref = "materialKolor" } 69 | kotlinLogging = { module = "io.github.oshai:kotlin-logging", version.ref = "kotlinLogging" } 70 | slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } 71 | slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" } 72 | 73 | [plugins] 74 | 75 | android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } 76 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 77 | jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } 78 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 79 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 80 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 81 | ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } 82 | hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } 83 | -------------------------------------------------------------------------------- /composeApp/src/commonTest/kotlin/com/joaomanaia/game2048/core/util/GridMovementTest.kt: -------------------------------------------------------------------------------- 1 | package com.joaomanaia.game2048.core.util 2 | 3 | import assertk.assertThat 4 | import assertk.assertions.containsExactlyInAnyOrder 5 | import assertk.assertions.isEmpty 6 | import assertk.assertions.isEqualTo 7 | import assertk.assertions.isTrue 8 | import com.joaomanaia.game2048.model.Cell 9 | import com.joaomanaia.game2048.model.Direction 10 | import com.joaomanaia.game2048.model.Grid 11 | import com.joaomanaia.game2048.model.GridTile 12 | import com.joaomanaia.game2048.model.GridTileMovement 13 | import com.joaomanaia.game2048.model.Tile 14 | import kotlin.test.BeforeTest 15 | import kotlin.test.Test 16 | 17 | internal class GridMovementTest { 18 | private lateinit var initialGrid: Grid 19 | 20 | @BeforeTest 21 | fun setup() { 22 | Tile.tileIdCounter = 0 23 | 24 | // The tile ids are dynamic generated, the table of generated ids are: 25 | // | 0 | - | - | 26 | // | 1 | 2 | - | 27 | // | 3 | 4 | - | 28 | initialGrid = listOf( 29 | listOf(Tile(16), null, null), 30 | listOf(Tile(2), Tile(2), null), 31 | listOf(Tile(8), Tile(4), null) 32 | ) 33 | } 34 | 35 | @Test 36 | fun testEmptyGrid() { 37 | val grid = emptyGrid(3) 38 | 39 | for (direction in Direction.entries) { 40 | val (newGrid, movements) = grid.makeMove(direction) 41 | 42 | assertThat(newGrid.isGridEmpty()).isTrue() 43 | assertThat(movements).isEmpty() 44 | } 45 | } 46 | 47 | @Test 48 | fun testDownTileMovement() { 49 | initialGrid.makeMove(Direction.DOWN).also { (newGridDown, movementsDown) -> 50 | assertThat(newGridDown).isEqualTo(initialGrid) 51 | with(initialGrid) { 52 | assertThat(movementsDown).containsExactlyInAnyOrder( 53 | createNoopMovementFromGrid(0, 0), 54 | createNoopMovementFromGrid(1, 0), 55 | createNoopMovementFromGrid(1, 1), 56 | createNoopMovementFromGrid(2, 0), 57 | createNoopMovementFromGrid(2, 1), 58 | ) 59 | } 60 | } 61 | } 62 | 63 | @Test 64 | fun testUpTileMovement() { 65 | initialGrid.makeMove(Direction.UP).also { (newGridUp, movementsUp) -> 66 | assertThat(newGridUp).isEqualTo( 67 | listOf( 68 | listOf(Tile(16, 0), Tile(2, 2), null), 69 | listOf(Tile(2, 1), Tile(4, 4), null), 70 | listOf(Tile(8, 3), null, null), 71 | ) 72 | ) 73 | with(initialGrid) { 74 | assertThat(movementsUp).containsExactlyInAnyOrder( 75 | createNoopMovementFromGrid(0, 0), 76 | createNoopMovementFromGrid(1, 0), 77 | createNoopMovementFromGrid(2, 0), 78 | createShiftMovementFromGrid(1, 1, 0, 1), 79 | createShiftMovementFromGrid(2, 1, 1, 1), 80 | ) 81 | } 82 | } 83 | } 84 | 85 | @Test 86 | fun testLeftTileMovement() { 87 | initialGrid.makeMove(Direction.LEFT).also { (newGridLeft, movementsLeft) -> 88 | assertThat(newGridLeft).isEqualTo( 89 | listOf( 90 | listOf(Tile(16, 0), null, null), 91 | listOf(Tile(4, 5), null, null), 92 | listOf(Tile(8, 3), Tile(4, 4), null) 93 | ) 94 | ) 95 | with(initialGrid) { 96 | assertThat(movementsLeft).containsExactlyInAnyOrder( 97 | createNoopMovementFromGrid(0, 0), 98 | createNoopMovementFromGrid(2, 0), 99 | createNoopMovementFromGrid(2, 1), 100 | createNoopMovementFromGrid(1, 0), 101 | createAddMovementFromGrid(4, 5,1, 0), 102 | createShiftMovementFromGrid(1, 1, 1, 0) 103 | ) 104 | } 105 | } 106 | } 107 | 108 | @Test 109 | fun testRightTileMovement() { 110 | initialGrid.makeMove(Direction.RIGHT).also { (newGridRight, movementsRight) -> 111 | assertThat(newGridRight).isEqualTo( 112 | listOf( 113 | listOf(null, null, Tile(16, 0)), 114 | listOf(null, null, Tile(4, 5)), 115 | listOf(null, Tile(8, 3), Tile(4, 4)) 116 | ) 117 | ) 118 | with(initialGrid) { 119 | assertThat(movementsRight).containsExactlyInAnyOrder( 120 | createShiftMovementFromGrid(0, 0, 0, 2), 121 | createShiftMovementFromGrid(2, 1, 2, 2), 122 | createShiftMovementFromGrid(2, 0, 2, 1), 123 | createShiftMovementFromGrid(1, 1, 1, 2), 124 | createShiftMovementFromGrid(1, 0, 1, 2), 125 | createAddMovementFromGrid(4, 5, 1, 2), 126 | ) 127 | } 128 | } 129 | } 130 | 131 | private fun createAddMovementFromGrid(tileNum: Int, tileId: Int, row: Int, col: Int): GridTileMovement { 132 | return GridTileMovement.add(GridTile(Cell(row, col), Tile(tileNum, tileId))) 133 | } 134 | 135 | private fun Grid.createShiftMovementFromGrid( 136 | fromRow: Int, 137 | fromCol: Int, 138 | toRow: Int, 139 | toCol: Int 140 | ): GridTileMovement { 141 | val tile = this[fromRow][fromCol]!! 142 | 143 | return GridTileMovement.shift( 144 | fromGridTile = GridTile(Cell(fromRow, fromCol), tile), 145 | toGridTile = GridTile(Cell(toRow, toCol), tile) 146 | ) 147 | } 148 | 149 | private fun Grid.createNoopMovementFromGrid(row: Int, col: Int): GridTileMovement { 150 | return GridTileMovement.noop(GridTile(Cell(row, col), this[row][col]!!)) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/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 | -------------------------------------------------------------------------------- /composeApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 2 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi 3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 4 | import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl 5 | 6 | plugins { 7 | alias(libs.plugins.kotlin.multiplatform) 8 | alias(libs.plugins.android.application) 9 | alias(libs.plugins.jetbrainsCompose) 10 | alias(libs.plugins.compose.compiler) 11 | alias(libs.plugins.kotlin.serialization) 12 | alias(libs.plugins.ksp) 13 | } 14 | 15 | kotlin { 16 | androidTarget { 17 | @OptIn(ExperimentalKotlinGradlePluginApi::class) 18 | compilerOptions { 19 | jvmTarget.set(JvmTarget.JVM_17) 20 | freeCompilerArgs.add("-opt-in=kotlin.RequiresOptIn") 21 | } 22 | } 23 | 24 | jvm("desktop") 25 | 26 | @OptIn(ExperimentalWasmDsl::class) 27 | wasmJs { 28 | moduleName = "composeApp" 29 | browser { 30 | commonWebpackConfig { 31 | outputFileName = "composeApp.js" 32 | } 33 | } 34 | binaries.executable() 35 | } 36 | 37 | applyDefaultHierarchyTemplate() 38 | 39 | jvmToolchain(17) 40 | 41 | sourceSets { 42 | commonMain { 43 | kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin") 44 | 45 | dependencies { 46 | implementation(compose.runtime) 47 | implementation(compose.foundation) 48 | implementation(compose.material3) 49 | implementation(compose.materialIconsExtended) 50 | implementation(compose.ui) 51 | implementation(compose.components.uiToolingPreview) 52 | implementation(compose.components.resources) 53 | 54 | implementation(libs.androidx.constraintlayout.compose) 55 | 56 | implementation(libs.kotlinx.coroutines.core) 57 | implementation(libs.kotlinx.serialization.json) 58 | 59 | implementation(libs.androidx.navigation.compose) 60 | 61 | // lifecycle runtime compose causing problems 62 | // implementation(libs.androidx.lifecycle.runtime.compose) 63 | implementation(libs.androidx.lifecycle.viewmodel.compose) 64 | 65 | api(project.dependencies.platform(libs.koin.bom)) 66 | api(libs.koin.core) 67 | implementation(libs.koin.compose) 68 | implementation(libs.koin.compose.viewmodel) 69 | 70 | implementation(libs.androidx.compose.material3.windowSizeClass) 71 | 72 | implementation(libs.slf4j.api) 73 | implementation(libs.slf4j.simple) 74 | implementation(libs.kotlinLogging) 75 | 76 | // Generate dynamic color scheme 77 | implementation(libs.materialKolor) 78 | } 79 | } 80 | 81 | commonTest.dependencies { 82 | implementation(kotlin("test")) 83 | implementation(kotlin("test-annotations-common")) 84 | implementation(libs.assertk) 85 | 86 | implementation(libs.kotlinx.coroutines.test) 87 | } 88 | 89 | val androidJvmMain by creating { 90 | dependsOn(commonMain.get()) 91 | 92 | dependencies { 93 | implementation(libs.androidx.datastore.preferences) 94 | } 95 | } 96 | 97 | androidMain { 98 | dependsOn(androidJvmMain) 99 | 100 | dependencies { 101 | implementation(libs.androidx.compose.ui.tooling.preview) 102 | implementation(libs.androidx.activity.compose) 103 | implementation(libs.androidx.core.splashscreen) 104 | 105 | implementation(libs.kotlinx.coroutines.android) 106 | 107 | implementation(libs.google.material) 108 | 109 | implementation(libs.koin.android) 110 | } 111 | } 112 | 113 | val desktopMain by getting { 114 | dependsOn(androidJvmMain) 115 | 116 | dependencies { 117 | implementation(compose.desktop.currentOs) 118 | 119 | implementation(libs.koin.logger.slf4j) 120 | 121 | implementation(libs.kotlinx.coroutines.swing) 122 | } 123 | } 124 | } 125 | } 126 | 127 | composeCompiler { 128 | enableStrongSkippingMode = true 129 | } 130 | 131 | tasks.withType { 132 | useJUnitPlatform() 133 | } 134 | 135 | android { 136 | namespace = "com.joaomanaia.game2048" 137 | compileSdk = 34 138 | 139 | defaultConfig { 140 | applicationId = "com.joaomanaia.game2048" 141 | minSdk = 21 142 | targetSdk = 34 143 | versionCode = 2 144 | versionName = "2.0.0" 145 | 146 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 147 | 148 | vectorDrawables { 149 | useSupportLibrary = true 150 | } 151 | } 152 | 153 | buildTypes { 154 | release { 155 | isMinifyEnabled = true 156 | isShrinkResources = true 157 | isDebuggable = false 158 | proguardFiles( 159 | getDefaultProguardFile("proguard-android.txt"), 160 | "proguard-compose-desktop.pro" 161 | ) 162 | } 163 | } 164 | compileOptions { 165 | sourceCompatibility = JavaVersion.VERSION_17 166 | targetCompatibility = JavaVersion.VERSION_17 167 | } 168 | buildFeatures { 169 | compose = true 170 | } 171 | packaging { 172 | resources { 173 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 174 | } 175 | } 176 | dependencies { 177 | debugImplementation(compose.uiTooling) 178 | } 179 | } 180 | 181 | compose.desktop { 182 | application { 183 | mainClass = "MainKt" 184 | 185 | nativeDistributions { 186 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 187 | packageName = "Game 2048" 188 | packageVersion = "2.0.0" 189 | 190 | buildTypes.release { 191 | proguard { 192 | configurationFiles.from(project.file("compose-desktop.pro")) 193 | obfuscate = true 194 | optimize = true 195 | } 196 | } 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------