diff --git a/app/src/main/java/io/zenandroid/onlinego/ai/KataGoAnalysisEngine.kt b/app/src/main/java/io/zenandroid/onlinego/ai/KataGoAnalysisEngine.kt index fc7c43af..e5f2da2b 100644 --- a/app/src/main/java/io/zenandroid/onlinego/ai/KataGoAnalysisEngine.kt +++ b/app/src/main/java/io/zenandroid/onlinego/ai/KataGoAnalysisEngine.kt @@ -15,10 +15,10 @@ import io.zenandroid.onlinego.data.model.katago.Query import io.zenandroid.onlinego.data.repositories.SettingsRepository import io.zenandroid.onlinego.gamelogic.Util import io.zenandroid.onlinego.utils.recordException +import org.koin.core.context.GlobalContext import java.io.* import java.util.* import java.util.concurrent.atomic.AtomicLong -import org.koin.core.context.GlobalContext.get object KataGoAnalysisEngine { var started = false diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/composables/BoardComposable.kt b/app/src/main/java/io/zenandroid/onlinego/ui/composables/BoardComposable.kt index 5277cab0..e1ba73a0 100644 --- a/app/src/main/java/io/zenandroid/onlinego/ui/composables/BoardComposable.kt +++ b/app/src/main/java/io/zenandroid/onlinego/ui/composables/BoardComposable.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.res.imageResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import io.zenandroid.onlinego.OnlineGoApplication import io.zenandroid.onlinego.data.model.BoardTheme import io.zenandroid.onlinego.data.model.Cell import io.zenandroid.onlinego.data.model.Position @@ -35,6 +36,7 @@ import io.zenandroid.onlinego.data.model.StoneType import io.zenandroid.onlinego.data.model.ogs.PlayCategory import io.zenandroid.onlinego.data.repositories.SettingsRepository import io.zenandroid.onlinego.gamelogic.RulesManager.isPass +import io.zenandroid.onlinego.utils.recordException import org.koin.core.context.GlobalContext import kotlin.math.ceil import kotlin.math.roundToInt @@ -68,6 +70,12 @@ fun Board( // Board background image, it is either a jpg or a svg val backgroundImage: ImageBitmap? = boardTheme.backgroundImage?.let { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + boardTheme.backgroundImage.run { + Exception("blep: $this, ${OnlineGoApplication.instance.getDrawable(this)}") + .let(::recordException) + } + } ImageBitmap.imageResource(id = boardTheme.backgroundImage) } val backgroundColor: Color? = boardTheme.backgroundColor?.let { diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/AiGameFragment.kt b/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/AiGameFragment.kt index 319f18a5..e4526c57 100644 --- a/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/AiGameFragment.kt +++ b/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/AiGameFragment.kt @@ -3,7 +3,6 @@ package io.zenandroid.onlinego.ui.screens.localai import android.app.Activity.RESULT_OK import android.content.Intent import android.graphics.Color -import android.graphics.Point import android.Manifest.permission import android.net.Uri import android.os.Bundle @@ -21,7 +20,6 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.transition.DrawableCrossFadeFactory -import com.jakewharton.rxbinding2.view.RxView import com.jakewharton.rxbinding3.view.clicks import com.toomasr.sgf4j.Sgf import com.toomasr.sgf4j.parser.Game @@ -30,6 +28,7 @@ import com.vmadalin.easypermissions.* import io.reactivex.Observable import io.reactivex.subjects.PublishSubject import io.zenandroid.onlinego.R +import io.zenandroid.onlinego.data.model.Cell import io.zenandroid.onlinego.data.model.local.SgfData import io.zenandroid.onlinego.data.model.Position import io.zenandroid.onlinego.data.model.StoneType @@ -153,6 +152,7 @@ class AiGameFragment : Fragment(), MviView { fadeOutRemovedStones = state.showFinalTerritory drawAiEstimatedOwnership = state.showAiEstimatedTerritory ownership = state.aiAnalysis?.ownership + hintBasis = if(state.showHints) state.aiAnalysis?.rootInfo else null hints = if(state.showHints) state.aiAnalysis?.moveInfos else null state.position?.let { position = it @@ -262,7 +262,7 @@ class AiGameFragment : Fragment(), MviView { val size = sgf?.getProperty("SZ")?.split(":")?.let { it.plus(it) }?.take(2) var pos = Position(size!![0].toInt(), size!![1].toInt()) - sgf?.getProperty("KM")?.toFloat()?.let { pos.komi = it } + sgf?.getProperty("KM")?.toFloat()?.let { pos = pos.copy(komi = it) } var handi = sgf?.getProperty("HA")?.toInt() var name = sgf?.getProperty("GN") ?: data.getPath() var move = sgf?.getRootNode()?.getNextNode() @@ -274,19 +274,22 @@ class AiGameFragment : Fragment(), MviView { else -> null }!! if (pos.nextToMove != colour) { - pos = RulesManager.makeMove(pos, pos.nextToMove, Point(-1, -1))!! - pos.nextToMove = pos.nextToMove.opponent + pos = RulesManager.makeMove(pos, pos.nextToMove, Cell(-1, -1))!!.let { + it.copy( + nextToMove = it.nextToMove.opponent + ) + } } pos = RulesManager.makeMove(pos, colour, if (move.getMoveString().isNullOrBlank()) { - Point(-1, -1) + Cell(-1, -1) } else { move.getCoords().let { - Point(it[0], it[1]) + Cell(it[0], it[1]) } } )!! - pos.nextToMove = pos.nextToMove.opponent + pos = pos.copy(nextToMove = pos.nextToMove.opponent) move = move.getNextNode() } Log.d("AiGameFragment", "loadPosition(\"${pos}\")") @@ -309,10 +312,7 @@ class AiGameFragment : Fragment(), MviView { } it.komi?.let { game.addProperty("KM", it.toString()) } - var positions = listOf(it) - while (positions.last()!!.parentPosition != null) { - positions = positions.plus(positions.last()!!.parentPosition!!) - } + val positions = state.history Log.d("AiGameFragment", "Serializing ${positions.size} moves") var cursor = game.getRootNode() diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/AiGameReducer.kt b/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/AiGameReducer.kt index 6008bfe9..ea41882d 100644 --- a/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/AiGameReducer.kt +++ b/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/AiGameReducer.kt @@ -19,10 +19,13 @@ import io.zenandroid.onlinego.ui.screens.localai.AiGameAction.GenerateAiMove import io.zenandroid.onlinego.ui.screens.localai.AiGameAction.HideOwnership import io.zenandroid.onlinego.ui.screens.localai.AiGameAction.NewGame import io.zenandroid.onlinego.ui.screens.localai.AiGameAction.NewPosition +import io.zenandroid.onlinego.ui.screens.localai.AiGameAction.NextPlayerChanged import io.zenandroid.onlinego.ui.screens.localai.AiGameAction.PromptUserForMove import io.zenandroid.onlinego.ui.screens.localai.AiGameAction.RestoredState import io.zenandroid.onlinego.ui.screens.localai.AiGameAction.ScoreComputed import io.zenandroid.onlinego.ui.screens.localai.AiGameAction.ShowNewGameDialog +import io.zenandroid.onlinego.ui.screens.localai.AiGameAction.ToggleAIBlack +import io.zenandroid.onlinego.ui.screens.localai.AiGameAction.ToggleAIWhite import io.zenandroid.onlinego.ui.screens.localai.AiGameAction.UserAskedForHint import io.zenandroid.onlinego.ui.screens.localai.AiGameAction.UserAskedForOwnership import io.zenandroid.onlinego.ui.screens.localai.AiGameAction.UserHotTrackedCoordinate @@ -176,7 +179,7 @@ class AiGameReducer : Reducer { chatText = "An error occurred communicating with the AI" ) UserPressedPrevious -> { - val newPosition = if(aiMovedLast(state) && !aiOnlyGame(state)) state.history.dropLast(2) + val newHistory = if(aiMovedLast(state) && !aiOnlyGame(state)) state.history.dropLast(2) else state.history.dropLast(1) val removedHistory = if(aiMovedLast(state) && !aiOnlyGame(state)) state.history.takeLast(2) else state.history.takeLast(1) diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/AnalyticsMiddleware.kt b/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/AnalyticsMiddleware.kt index e62d28ac..037c59df 100644 --- a/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/AnalyticsMiddleware.kt +++ b/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/AnalyticsMiddleware.kt @@ -62,6 +62,9 @@ class AnalyticsMiddleware: Middleware { UserAskedForOwnership -> analytics.logEvent("ai_game_user_asked_territory", null) is UserTriedSuicidalMove -> analytics.logEvent("ai_game_user_tried_suicide", null) is UserTriedKoMove -> analytics.logEvent("ai_game_user_tried_ko", null) + NextPlayerChanged -> analytics.logEvent("ai_game_next_player_changed", null) + ToggleAIBlack -> analytics.logEvent("ai_game_toggled_ai_black", null) + ToggleAIWhite -> analytics.logEvent("ai_game_toggled_ai_white", null) } } .switchMap { Observable.empty() } diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/screens/settings/SettingsFragment.kt b/app/src/main/java/io/zenandroid/onlinego/ui/screens/settings/SettingsFragment.kt index 42f6f910..2b9d874e 100644 --- a/app/src/main/java/io/zenandroid/onlinego/ui/screens/settings/SettingsFragment.kt +++ b/app/src/main/java/io/zenandroid/onlinego/ui/screens/settings/SettingsFragment.kt @@ -34,6 +34,7 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedTextField import androidx.compose.material.Surface import androidx.compose.material.Switch +import androidx.compose.material.Slider import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.material.icons.Icons @@ -44,11 +45,13 @@ import androidx.compose.material.icons.filled.Notifications import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material.icons.filled.VolumeUp +import androidx.compose.material.icons.rounded.AccountTree import androidx.compose.material.icons.rounded.DarkMode import androidx.compose.material.icons.rounded.HeartBroken import androidx.compose.material.icons.rounded.Logout import androidx.compose.material.icons.rounded.MilitaryTech import androidx.compose.material.icons.rounded.Palette +import androidx.compose.material.icons.rounded.Psychology import androidx.compose.material.icons.rounded._123 import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable @@ -97,7 +100,9 @@ import io.zenandroid.onlinego.ui.screens.settings.SettingsAction.CoordinatesClic import io.zenandroid.onlinego.ui.screens.settings.SettingsAction.DeleteAccountCanceled import io.zenandroid.onlinego.ui.screens.settings.SettingsAction.DeleteAccountClicked import io.zenandroid.onlinego.ui.screens.settings.SettingsAction.DeleteAccountConfirmed +import io.zenandroid.onlinego.ui.screens.settings.SettingsAction.DetailedAnalysisClicked import io.zenandroid.onlinego.ui.screens.settings.SettingsAction.LogoutClicked +import io.zenandroid.onlinego.ui.screens.settings.SettingsAction.MaxVisitsChanged import io.zenandroid.onlinego.ui.screens.settings.SettingsAction.NotificationsClicked import io.zenandroid.onlinego.ui.screens.settings.SettingsAction.PrivacyClicked import io.zenandroid.onlinego.ui.screens.settings.SettingsAction.RanksClicked @@ -407,6 +412,24 @@ fun SettingsScreen(state: SettingsState, onAction: (SettingsAction) -> Unit) { ) } } + Section(title = "Engine Settings") { + Column(modifier = Modifier) { + SettingsRow( + title = "Max AI Playouts", + icon = Rounded.AccountTree, + slider = Pair(10.0, 10000.0), + position = state.maxVisits, + onValueChanged = { onAction(MaxVisitsChanged(it)) } + ) + SettingsRow( + title = "Detailed AI Analysis", + icon = Rounded.Psychology, + checkbox = true, + checked = state.detailedAnalysis, + onClick = { onAction(DetailedAnalysisClicked) } + ) + } + } Section(title = "Account") { Column(modifier = Modifier) { SettingsRow( @@ -453,10 +476,13 @@ private fun SettingsRow( icon: ImageVector, checkbox: Boolean = false, checked: Boolean = false, + slider: Pair? = null, + position: Double = 0.0, value: String? = null, possibleValues: List = emptyList(), onClick: () -> Unit = {}, onValueClick: (String) -> Unit = {}, + onValueChanged: (Double) -> Unit = {}, ) { var menuOpen by remember { mutableStateOf(false) } Row( @@ -488,9 +514,27 @@ private fun SettingsRow( if(checkbox) { Switch( checked = checked, - onCheckedChange = { onClick()}, + onCheckedChange = { onClick() }, modifier = Modifier.padding(end = 12.dp) ) + } else if(slider != null) { + Slider( + value = Math.log(position).toFloat(), + onValueChange = { onValueChanged(Math.exp(it.toDouble())) }, + steps = 10, + valueRange = Math.log(slider.first).toFloat()..Math.log(slider.second).toFloat(), + modifier = Modifier.weight(1f).padding(end = 12.dp) + ) + Text( + text = position.toInt().toString().padStart(8), + fontSize = 14.sp, + style = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + letterSpacing = 0.4.sp + ), + modifier = Modifier.padding(end = 16.dp, bottom = 16.dp, top = 16.dp) + ) } else if(value != null) { Box { Text( @@ -583,4 +627,4 @@ private fun SettingsScreenPreview() { ), {}) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/screens/settings/SettingsViewModel.kt b/app/src/main/java/io/zenandroid/onlinego/ui/screens/settings/SettingsViewModel.kt index 2e9f977a..0eedbb61 100644 --- a/app/src/main/java/io/zenandroid/onlinego/ui/screens/settings/SettingsViewModel.kt +++ b/app/src/main/java/io/zenandroid/onlinego/ui/screens/settings/SettingsViewModel.kt @@ -13,7 +13,9 @@ import io.zenandroid.onlinego.ui.screens.settings.SettingsAction.CoordinatesClic import io.zenandroid.onlinego.ui.screens.settings.SettingsAction.DeleteAccountCanceled import io.zenandroid.onlinego.ui.screens.settings.SettingsAction.DeleteAccountClicked import io.zenandroid.onlinego.ui.screens.settings.SettingsAction.DeleteAccountConfirmed +import io.zenandroid.onlinego.ui.screens.settings.SettingsAction.DetailedAnalysisClicked import io.zenandroid.onlinego.ui.screens.settings.SettingsAction.LogoutClicked +import io.zenandroid.onlinego.ui.screens.settings.SettingsAction.MaxVisitsChanged import io.zenandroid.onlinego.ui.screens.settings.SettingsAction.NotificationsClicked import io.zenandroid.onlinego.ui.screens.settings.SettingsAction.PrivacyClicked import io.zenandroid.onlinego.ui.screens.settings.SettingsAction.RanksClicked @@ -37,6 +39,8 @@ class SettingsViewModel( sounds = settingsRepository.sound, ranks = settingsRepository.showRanks, coordinates = settingsRepository.showCoordinates, + maxVisits = settingsRepository.maxVisits.toDouble(), + detailedAnalysis = settingsRepository.detailedAnalysis, username = userSessionRepository.uiConfig?.user?.username ?: "", avatarURL = userSessionRepository.uiConfig?.user?.icon, ) @@ -54,6 +58,16 @@ class SettingsViewModel( state.update { it.copy(ranks = !it.ranks) } } + is MaxVisitsChanged -> { + settingsRepository.maxVisits = action.value.toInt() + state.update { it.copy(maxVisits = action.value) } + } + + DetailedAnalysisClicked -> { + settingsRepository.detailedAnalysis = !settingsRepository.detailedAnalysis + state.update { it.copy(detailedAnalysis = !it.detailedAnalysis) } + } + SoundsClicked -> { settingsRepository.sound = !state.value.sounds state.update { it.copy(sounds = !it.sounds) } @@ -158,6 +172,8 @@ data class SettingsState( val sounds: Boolean = true, val ranks: Boolean = true, val coordinates: Boolean = true, + val maxVisits: Double = 30.0, + val detailedAnalysis: Boolean = false, val username: String = "", val avatarURL: String? = null, val passwordDialogVisible: Boolean = false, @@ -171,6 +187,8 @@ sealed interface SettingsAction { data class ThemeClicked(val theme: String) : SettingsAction data class BoardThemeClicked(val boardDisplayName: String) : SettingsAction data object CoordinatesClicked : SettingsAction + data class MaxVisitsChanged(val value: Double) : SettingsAction + data object DetailedAnalysisClicked : SettingsAction data object RanksClicked : SettingsAction data object LogoutClicked : SettingsAction data object DeleteAccountClicked : SettingsAction @@ -178,4 +196,4 @@ sealed interface SettingsAction { data class DeleteAccountConfirmed(val password: String) : SettingsAction data object PrivacyClicked : SettingsAction data object SupportClicked : SettingsAction -} \ No newline at end of file +} diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/views/BoardView.kt b/app/src/main/java/io/zenandroid/onlinego/ui/views/BoardView.kt index 20431cbf..f9e63de0 100644 --- a/app/src/main/java/io/zenandroid/onlinego/ui/views/BoardView.kt +++ b/app/src/main/java/io/zenandroid/onlinego/ui/views/BoardView.kt @@ -27,9 +27,9 @@ import io.zenandroid.onlinego.data.model.StoneType import io.zenandroid.onlinego.data.model.katago.MoveInfo import io.zenandroid.onlinego.data.model.ogs.PlayCategory import io.zenandroid.onlinego.gamelogic.Util -import io.zenandroid.onlinego.data.model.katago.MoveInfo import io.zenandroid.onlinego.data.model.katago.RootInfo import io.zenandroid.onlinego.data.repositories.SettingsRepository +import org.koin.core.context.GlobalContext import java.util.* import kotlin.math.abs import kotlin.math.ceil @@ -42,7 +42,7 @@ import kotlin.math.roundToInt * that is passed to it via setPosition() */ class BoardView : View { - private val settingsRepository: SettingsRepository = get().get() + private val settingsRepository: SettingsRepository = GlobalContext.get().get() var boardWidth = 19 set(boardWidth) { @@ -154,6 +154,7 @@ class BoardView : View { } field = value } + var hintBasis: RootInfo? = null var hints: List? = null var ownership: List? = null @@ -451,16 +452,17 @@ class BoardView : View { private fun drawHints(canvas: Canvas, position: Position) { hints?.let { - //val root = position.aiAnalysisResult?.rootInfo val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() - //val adapter = moshi.adapter(RootInfo::class.java) - val adapter = moshi.adapter(MoveInfo::class.java) + val adapter = moshi.adapter(RootInfo::class.java) + Log.d("BoardView", "Root: ${hintBasis?.let { adapter.toJson(it) }}") for((index, hint) in it.withIndex()) { - val winrateHighest = hints?.map { it.winrate }.filterNotNull().maxOrNull() ?: 100f - val winrateLowest = hints?.map { it.winrate }.filterNotNull().minOrNull() ?: 0f + val adapter = moshi.adapter(MoveInfo::class.java) + Log.d("BoardView", "Hint: ${hint?.let { adapter.toJson(it) }}") + val winrateHighest = hints?.map { it.winrate }.orEmpty().maxOrNull() ?: 100f + val winrateLowest = hints?.map { it.winrate }.orEmpty().minOrNull() ?: 0f val winrate = hint.winrate * 100 val playouts = hint.visits - val blackScoreDiff = hint.scoreLead.minus(root?.scoreLead ?: 0f) + val blackScoreDiff = hint.scoreLead.minus(hintBasis?.scoreLead ?: 0f) val scoreDiff = blackScoreDiff * if(position.nextToMove == StoneType.WHITE) -1 else 1 @@ -490,7 +492,7 @@ class BoardView : View { Log.d("BoardView", "Prediction: ${adapter.toJson(hint)}") val height = aiTextPaint.getFontMetrics().let { it.ascent - it.descent } drawTextCentred(canvas, aiTextPaint, "${String.format("%.2g", scoreDiff)}", center.x, center.y - height) - drawTextCentred(canvas, aiTextPaint, "#${index + 1} | ${playouts} ", center.x, center.y) + drawTextCentred(canvas, aiTextPaint, "#${index + 1} | ${playouts}x ", center.x, center.y) aiTextPaint.let { it.setTypeface(Typeface.create(it.getTypeface(), Typeface.BOLD)) } diff --git a/app/src/main/res/drawable/ic_menu.xml b/app/src/main/res/drawable/ic_menu.xml new file mode 100644 index 00000000..8ad7b0cd --- /dev/null +++ b/app/src/main/res/drawable/ic_menu.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_menu_hamburger.xml b/app/src/main/res/drawable/ic_menu_hamburger.xml new file mode 100644 index 00000000..8ad7b0cd --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_hamburger.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_aigame.xml b/app/src/main/res/layout/fragment_aigame.xml index e47df238..a12e43ff 100644 --- a/app/src/main/res/layout/fragment_aigame.xml +++ b/app/src/main/res/layout/fragment_aigame.xml @@ -334,7 +334,7 @@ android:foreground="?android:attr/selectableItemBackground" android:paddingTop="10dp" android:paddingBottom="10dp" - app:srcCompat="@drawable/ic_menu" + app:srcCompat="@drawable/ic_menu_hamburger" app:tint="@color/disable_black" />