diff --git a/app/build.gradle b/app/build.gradle
index 111722c4..60d51bda 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -197,6 +197,18 @@ dependencies {
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
+ implementation 'com.github.sephiroth74:NumberSlidingPicker:v1.0.3'
+
+ implementation 'com.github.tony19:logback-android:2.0.0'
+ implementation('com.github.toomasr:sgf4j:sgf4j-parser-0.0.6') {
+ // Force to use android logging
+ exclude group: 'org.apache.logging.log4j', module: 'log4j-slf4j-impl'
+ exclude group: 'org.apache.logging.log4j', module: 'log4j-api'
+ exclude group: 'org.apache.logging.log4j', module: 'log4j-core'
+ }
+
+ implementation 'com.vmadalin:easypermissions-ktx:1.0.0'
+
testImplementation 'com.nhaarman:mockito-kotlin-kt1.1:1.6.0'
testImplementation 'junit:junit:4.13.2'
testImplementation "io.insert-koin:koin-test-junit4:$koin_version"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a23274dc..7ca3c3bc 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,6 +6,8 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
,
komi: Float? = null,
- maxVisits: Int? = null,
+ maxVisits: Int? = settingsRepository.maxVisits,
includeOwnership: Boolean? = null,
includeMovesOwnership: Boolean? = null,
includePolicy: Boolean? = null
diff --git a/app/src/main/java/io/zenandroid/onlinego/data/model/local/SgfData.kt b/app/src/main/java/io/zenandroid/onlinego/data/model/local/SgfData.kt
new file mode 100644
index 00000000..146f3bfa
--- /dev/null
+++ b/app/src/main/java/io/zenandroid/onlinego/data/model/local/SgfData.kt
@@ -0,0 +1,27 @@
+package io.zenandroid.onlinego.data.model.local
+
+import android.util.Log
+import io.zenandroid.onlinego.data.model.Position
+import io.zenandroid.onlinego.data.model.ogs.GameData
+import io.zenandroid.onlinego.data.model.ogs.OGSGame
+import io.zenandroid.onlinego.data.model.ogs.Phase
+import io.zenandroid.onlinego.data.ogs.TimeControl
+import io.zenandroid.onlinego.utils.toEpochMicros
+
+data class SgfData(
+ val name: String?,
+ var position: Position?,
+ var handicap: Int?,
+ val rules: String?
+) {
+ companion object {
+ fun fromString(game: String): SgfData {
+ return SgfData(
+ name = null,
+ handicap = null,
+ rules = null,
+ position = null
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/io/zenandroid/onlinego/data/repositories/SettingsRepository.kt b/app/src/main/java/io/zenandroid/onlinego/data/repositories/SettingsRepository.kt
index 735e9066..3ea89862 100644
--- a/app/src/main/java/io/zenandroid/onlinego/data/repositories/SettingsRepository.kt
+++ b/app/src/main/java/io/zenandroid/onlinego/data/repositories/SettingsRepository.kt
@@ -9,6 +9,8 @@ private const val BOARD_THEME = "board_theme"
private const val SHOW_RANKS = "show_ranks"
private const val SHOW_COORDINATES = "show_coordinates"
private const val SOUND = "sound"
+private const val MAX_VISITS = "max_ai_visits"
+private const val DETAILED_ANALYSIS = "detailed_analysis"
class SettingsRepository {
private val prefs = PreferenceManager.getDefaultSharedPreferences(OnlineGoApplication.instance.baseContext)
@@ -44,4 +46,12 @@ class SettingsRepository {
var sound: Boolean
get() = prefs.getBoolean(SOUND, true)
set(value) = prefs.edit().putBoolean(SOUND, value).apply()
-}
\ No newline at end of file
+
+ var maxVisits: Int
+ get() = prefs.getInt(MAX_VISITS, 30)
+ set(value) = prefs.edit().putInt(MAX_VISITS, value).apply()
+
+ var detailedAnalysis: Boolean
+ get() = prefs.getBoolean(DETAILED_ANALYSIS, false)
+ set(value) = prefs.edit().putBoolean(DETAILED_ANALYSIS, value).apply()
+}
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/AiGameAction.kt b/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/AiGameAction.kt
index ca1dc6c1..13b1d0c8 100644
--- a/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/AiGameAction.kt
+++ b/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/AiGameAction.kt
@@ -4,9 +4,10 @@ import io.zenandroid.onlinego.data.model.Cell
import io.zenandroid.onlinego.data.model.Position
import io.zenandroid.onlinego.data.model.katago.KataGoResponse.Response
import io.zenandroid.onlinego.data.model.katago.MoveInfo
+import io.zenandroid.onlinego.data.model.local.SgfData
sealed class AiGameAction {
- object ViewReady: AiGameAction()
+ class ViewReady(val loadData: SgfData?, val savedData: String?): AiGameAction()
object ViewPaused: AiGameAction()
class RestoredState(val state: AiGameState): AiGameAction()
object CantRestoreState: AiGameAction()
@@ -16,6 +17,7 @@ sealed class AiGameAction {
class NewGame(
val size: Int,
val youPlayBlack: Boolean,
+ val youPlayWhite: Boolean,
val handicap: Int
): AiGameAction()
@@ -28,11 +30,12 @@ sealed class AiGameAction {
class NewPosition(val newPos: Position): AiGameAction()
class AIMove(val newPos: Position, val aiAnalisis: Response, val selectedMove: MoveInfo): AiGameAction()
+ object NextPlayerChanged: AiGameAction()
object AIError: AiGameAction()
class AIHint(val aiAnalisis: Response): AiGameAction()
class AIOwnershipResponse(val aiAnalisis: Response): AiGameAction()
object HideOwnership: AiGameAction()
- class ScoreComputed(val newPos: Position, val whiteScore: Float, val blackScore: Int, val aiWon: Boolean, val aiAnalisis: Response): AiGameAction()
+ class ScoreComputed(val newPos: Position, val whiteScore: Float, val blackScore: Int, val whiteWon: Boolean, val aiAnalisis: Response): AiGameAction()
// User actions
@@ -46,6 +49,7 @@ sealed class AiGameAction {
object UserAskedForOwnership: AiGameAction()
class UserTriedSuicidalMove(val coordinate: Cell): AiGameAction()
class UserTriedKoMove(val coordinate: Cell): AiGameAction()
+ object ToggleAIBlack: AiGameAction()
+ object ToggleAIWhite: AiGameAction()
-
-}
\ No newline at end of file
+}
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 f3c7d5d2..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
@@ -1,7 +1,12 @@
package io.zenandroid.onlinego.ui.screens.localai
+import android.app.Activity.RESULT_OK
+import android.content.Intent
import android.graphics.Color
+import android.Manifest.permission
+import android.net.Uri
import android.os.Bundle
+import android.os.StrictMode
import android.util.Log
import android.view.LayoutInflater
import android.view.View
@@ -16,16 +21,28 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.transition.DrawableCrossFadeFactory
import com.jakewharton.rxbinding3.view.clicks
+import com.toomasr.sgf4j.Sgf
+import com.toomasr.sgf4j.parser.Game
+import com.toomasr.sgf4j.parser.GameNode
+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
import io.zenandroid.onlinego.data.repositories.SettingsRepository
import io.zenandroid.onlinego.data.repositories.UserSessionRepository
import io.zenandroid.onlinego.databinding.FragmentAigameBinding
+import io.zenandroid.onlinego.gamelogic.RulesManager
+import io.zenandroid.onlinego.gamelogic.Util.getSGFCoordinates
import io.zenandroid.onlinego.mvi.MviView
import io.zenandroid.onlinego.ui.screens.localai.AiGameAction.DismissNewGameDialog
import io.zenandroid.onlinego.ui.screens.localai.AiGameAction.NewGame
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.UserPressedNext
@@ -36,6 +53,11 @@ import io.zenandroid.onlinego.ui.screens.localai.AiGameAction.ViewReady
import io.zenandroid.onlinego.utils.analyticsReportScreen
import io.zenandroid.onlinego.utils.processGravatarURL
import io.zenandroid.onlinego.utils.showIf
+import java.io.BufferedReader
+import java.io.File
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
import org.koin.android.ext.android.get
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
@@ -45,6 +67,8 @@ class AiGameFragment : Fragment(), MviView {
private val viewModel: AiGameViewModel by viewModel()
private val settingsRepository: SettingsRepository by inject()
private var bottomSheet: NewGameBottomSheet? = null
+ private var setupSgf: SgfData? = null
+ private var savedSgf: String? = null
private lateinit var binding: FragmentAigameBinding
private val internalActions = PublishSubject.create()
@@ -68,9 +92,16 @@ class AiGameFragment : Fragment(), MviView {
binding.hintButton.clicks()
.map { UserAskedForHint },
binding.ownershipButton.clicks()
- .map { UserAskedForOwnership }
+ .map { UserAskedForOwnership },
+ binding.nameButtonLeft.clicks()
+ .map { ToggleAIBlack },
+ binding.nameButtonRight.clicks()
+ .map { ToggleAIWhite }
)
- ).startWith(ViewReady)
+ ).startWith(ViewReady(setupSgf, savedSgf).also {
+ setupSgf = null
+ savedSgf = null
+ })
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentAigameBinding.inflate(inflater, container, false)
@@ -86,11 +117,14 @@ class AiGameFragment : Fragment(), MviView {
override fun onResume() {
super.onResume()
analyticsReportScreen("AiGame")
+
+ getArguments()?.getString("SGF_LOCAL")?.let { loadSGF(Uri.parse(it)) }
+ getArguments()?.getString("SGF_REMOTE")?.let { loadSGF(Uri.parse(it)) }
+
binding.board.apply {
drawCoordinates = settingsRepository.showCoordinates
}
-
view?.doOnLayout {
binding.iconContainerLeft.radius = binding.iconContainerLeft.width / 2f
binding.iconContainerRight.radius = binding.iconContainerRight.width / 2f
@@ -118,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
@@ -130,10 +165,44 @@ class AiGameFragment : Fragment(), MviView {
binding.hintButton.showIf(state.hintButtonVisible)
binding.ownershipButton.showIf(state.ownershipButtonVisible)
+ binding.nameButtonLeft.setText(if(state.enginePlaysBlack) "KataGo" else "Player")
+ binding.nameButtonRight.setText(if(state.enginePlaysWhite) "KataGo" else "Player")
if(state.newGameDialogShown && bottomSheet?.isShowing != true) {
- bottomSheet = NewGameBottomSheet(requireContext()) { size, youPlayBlack, handicap ->
- internalActions.onNext(NewGame(size, youPlayBlack, handicap))
- }.apply {
+ bottomSheet = NewGameBottomSheet(requireContext(), { size, youPlayBlack, youPlayWhite, handicap ->
+ internalActions.onNext(NewGame(size, youPlayBlack, youPlayWhite, handicap))
+ }, {
+ if (EasyPermissions.hasPermissions(requireContext(), permission.READ_EXTERNAL_STORAGE)) {
+ var chooseFile = Intent(Intent.ACTION_GET_CONTENT)
+ chooseFile.setType("application/x-go-sgf")
+ chooseFile = Intent.createChooser(chooseFile, "Choose a file")
+ startActivityForResult(chooseFile, 1)
+ internalActions.onNext(DismissNewGameDialog)
+ } else {
+ EasyPermissions.requestPermissions(
+ host = this,
+ rationale = "App needs Read Storage permission to load files",
+ requestCode = -1,
+ permission.READ_EXTERNAL_STORAGE
+ )
+ }
+ }, {
+ if (EasyPermissions.hasPermissions(requireContext(), permission.WRITE_EXTERNAL_STORAGE)) {
+ var chooseFile = Intent(Intent.ACTION_CREATE_DOCUMENT);
+ chooseFile.addCategory(Intent.CATEGORY_OPENABLE)
+ chooseFile.setType("application/x-go-sgf")
+ chooseFile.putExtra(Intent.EXTRA_TITLE, "go.sgf")
+ saveSGF(state)
+ startActivityForResult(chooseFile, 2)
+ internalActions.onNext(DismissNewGameDialog)
+ } else {
+ EasyPermissions.requestPermissions(
+ host = this,
+ rationale = "App needs Write Storage permission to load files",
+ requestCode = -1,
+ permission.WRITE_EXTERNAL_STORAGE
+ )
+ }
+ }).apply {
setOnCancelListener {
internalActions.onNext(DismissNewGameDialog)
}
@@ -150,13 +219,13 @@ class AiGameFragment : Fragment(), MviView {
binding.winrateProgressBar.progress = winrateAsPercentage.toInt()
}
state.position?.let {
- binding.prisonersLeft.text = if(state.enginePlaysBlack) it.blackCaptureCount.toString() else it.whiteCaptureCount.toString()
- binding.prisonersRight.text = if(state.enginePlaysBlack) it.whiteCaptureCount.toString() else it.blackCaptureCount.toString()
- binding.komiLeft.text = if(state.enginePlaysBlack) "" else it.komi.toString()
- binding.komiRight.text = if(state.enginePlaysBlack) it.komi.toString() else ""
+ binding.prisonersLeft.text = it.blackCaptureCount.toString()
+ binding.prisonersRight.text = it.whiteCaptureCount.toString()
+ binding.komiLeft.text = "-"
+ binding.komiRight.text = it.komi.toString()
}
- binding.colorIndicatorLeft.setColorFilter(if(state.enginePlaysBlack) Color.BLACK else Color.WHITE)
- binding.colorIndicatorRight.setColorFilter(if(state.enginePlaysBlack) Color.WHITE else Color.BLACK)
+ binding.colorIndicatorLeft.setColorFilter(Color.BLACK)
+ binding.colorIndicatorRight.setColorFilter(Color.WHITE)
state.chatText?.let {
binding.chatBubble.visibility = VISIBLE
@@ -170,4 +239,126 @@ class AiGameFragment : Fragment(), MviView {
binding.scoreleadLabel.text = "Score prediction: $leader leads by $lead"
}
}
-}
\ No newline at end of file
+
+ private fun loadSGF(data: Uri) {
+ val stream = when(data.getScheme()) {
+ "http", "https" -> OkHttpClient.Builder()
+ .cookieJar(get().cookieJar)
+ .followRedirects(true)
+ .build().let { client ->
+ val threadPolicy = StrictMode.ThreadPolicy.Builder().permitAll().build()
+ StrictMode.setThreadPolicy(threadPolicy) // UI thread for this intent only
+
+ val request = Request.Builder().url(data.toString()).build()
+ val response = client.newCall(request).execute()
+ response?.body?.byteStream()
+ }
+ else -> requireContext().getContentResolver().openInputStream(data)
+ }
+ val text = stream?.bufferedReader()?.use(BufferedReader::readText)
+ Log.d("AiGameFragment", "onLoad(\"${data}\") = \"${text}\"")
+ val sgf = text?.let { Sgf.createFromString(it) }
+ Log.d("AiGameFragment", "SGF ${sgf.toString()}")
+
+ 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 = pos.copy(komi = it) }
+ var handi = sgf?.getProperty("HA")?.toInt()
+ var name = sgf?.getProperty("GN") ?: data.getPath()
+ var move = sgf?.getRootNode()?.getNextNode()
+ while (move != null) {
+ Log.d("AiGameFragment", "makeMove(\"${move}\")")
+ val colour = when(move.getColor()) {
+ "W" -> StoneType.WHITE
+ "B" -> StoneType.BLACK
+ else -> null
+ }!!
+ if (pos.nextToMove != colour) {
+ 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()) {
+ Cell(-1, -1)
+ } else {
+ move.getCoords().let {
+ Cell(it[0], it[1])
+ }
+ }
+ )!!
+ pos = pos.copy(nextToMove = pos.nextToMove.opponent)
+ move = move.getNextNode()
+ }
+ Log.d("AiGameFragment", "loadPosition(\"${pos}\")")
+ setupSgf = SgfData(
+ position = pos,
+ handicap = handi,
+ name = name,
+ rules = null
+ )
+ }
+
+ private fun saveSGF(state: AiGameState) {
+ val game = Game()
+ state.handicap?.let { game.addProperty("HA", it.toString()) }
+ state.position?.let {
+ if(it.boardWidth == it.boardHeight) {
+ game.addProperty("SZ", "${it.boardWidth}")
+ } else {
+ game.addProperty("SZ", "${it.boardWidth}:${it.boardHeight}")
+ }
+ it.komi?.let { game.addProperty("KM", it.toString()) }
+
+ val positions = state.history
+ Log.d("AiGameFragment", "Serializing ${positions.size} moves")
+
+ var cursor = game.getRootNode()
+ positions.reversed().forEach { position ->
+ Log.d("AiGameFragment", "Serializing ${position}")
+ position.lastMove?.let {
+ if (it.x == -1 || it.y == -1) {
+ Log.d("AiGameFragment", "mkPass")
+ } else {
+ val node = GameNode(cursor)
+ if (cursor == null) game.setRootNode(node)
+ else cursor.addChild(node)
+ cursor = node
+ val colour = when(position.lastPlayerToMove) {
+ StoneType.WHITE -> "W"
+ StoneType.BLACK -> "B"
+ else -> null
+ }!!
+ node.addProperty(colour, getSGFCoordinates(it))
+ Log.d("AiGameFragment", "mkNode(${node})")
+ }
+ }
+ }
+ }
+ Log.d("AiGameFragment", "sgf(${game.getGeneratedSgf()})")
+ savedSgf = game.getGeneratedSgf()
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ if(requestCode == 1 && resultCode == RESULT_OK)
+ data?.getData()?.let {
+ loadSGF(it)
+ }
+ if(requestCode == 2 && resultCode == RESULT_OK)
+ data?.getData()?.let {
+ Log.d("AiGameFragment", "onSave(\"${it}\") = \"${savedSgf}\"")
+ val stream = requireContext().getContentResolver().openOutputStream(it)
+ stream?.bufferedWriter()?.use { out -> out.write(savedSgf!!) }
+ }
+ }
+
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+
+ // EasyPermissions handles the request result.
+ EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this)
+ }
+}
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 d5392b3b..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
@@ -1,6 +1,8 @@
package io.zenandroid.onlinego.ui.screens.localai
import android.util.Log
+import io.zenandroid.onlinego.data.model.Position
+import io.zenandroid.onlinego.data.model.StoneType
import io.zenandroid.onlinego.gamelogic.RulesManager
import io.zenandroid.onlinego.gamelogic.RulesManager.isGameOver
import io.zenandroid.onlinego.mvi.Reducer
@@ -17,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
@@ -50,9 +55,30 @@ class AiGameReducer : Reducer {
ViewPaused, UserPressedBack, UserPressedPass -> {
state
}
- ViewReady -> state.copy(
- chatText = "Give me a second, I'm getting ready..."
- )
+ is ViewReady -> state.copy(
+ chatText = "Give me a second, I'm getting ready...",
+ ).let {
+ action.loadData?.let { data ->
+ Log.d("AiGameReducer", "Game Loaded")
+ it.copy(
+ position = data.position,
+ boardSize = when(data.position?.boardWidth) {
+ data.position?.boardHeight -> data.position?.boardWidth
+ else -> null //nonsquare
+ }!!,
+ handicap = data.handicap ?: 0,
+ enginePlaysBlack = false,
+ enginePlaysWhite = false,
+ chatText = "Game Loaded!",
+ redoPosStack = emptyList()
+ )
+ } ?: action.savedData?.let { data ->
+ Log.d("AiGameReducer", "Game Saved")
+ it.copy(
+ chatText = "Game Saved!",
+ )
+ } ?: it
+ }
is NewPosition -> {
val newVariation = if(state.history.lastOrNull() == action.newPos) state.history else state.history + action.newPos
state.copy(
@@ -63,13 +89,13 @@ class AiGameReducer : Reducer {
boardIsInteractive = false,
showHints = false,
chatText = when {
- newVariation.isGameOver() && state.aiWon == true -> "Game ended because of two passes. Final score is black ${state.finalBlackScore?.toInt()} to white ${state.finalWhiteScore}. Looks like I win this time."
- newVariation.isGameOver() && state.aiWon == false -> "Game ended because of two passes. Final score is black ${state.finalBlackScore?.toInt()} to white ${state.finalWhiteScore}. Congrats, looks like you got the better of me."
- newVariation.isGameOver() && state.aiWon == null -> "Game ended because of two passes. Hang on, I'm computing the final score."
+ newVariation.isGameOver() && state.whiteWon == true -> "Game ended because of two passes. Final score is black ${state.finalBlackScore?.toInt()} to white ${state.finalWhiteScore}. White wins!"
+ newVariation.isGameOver() && state.whiteWon == false -> "Game ended because of two passes. Final score is black ${state.finalBlackScore?.toInt()} to white ${state.finalWhiteScore}. Black wins!"
+ newVariation.isGameOver() && state.whiteWon == null -> "Game ended because of two passes. Hang on, I'm computing the final score."
else -> state.chatText
},
showAiEstimatedTerritory = false,
- showFinalTerritory = newVariation.isGameOver() && state.aiWon != null,
+ showFinalTerritory = newVariation.isGameOver() && state.whiteWon != null,
hintButtonVisible = !newVariation.isGameOver(),
ownershipButtonVisible = !newVariation.isGameOver()
)
@@ -81,11 +107,11 @@ class AiGameReducer : Reducer {
passButtonEnabled = false,
redoPosStack = emptyList(),
boardIsInteractive = false,
- chatText = if (action.aiWon) "Game ended because of two passes. Final score is black ${action.blackScore} to white ${action.whiteScore}. Looks like I win this time."
- else "Game ended because of two passes. Final score is black ${action.blackScore} to white ${action.whiteScore}. Congrats, looks like you got the better of me.",
- finalWhiteScore = action.whiteScore,
+ chatText = if (action.whiteWon) "Game ended because of two passes. Final score is black ${action.blackScore} to white ${action.whiteScore}. White wins!"
+ else "Game ended because of two passes. Final score is black ${action.blackScore} to white ${action.whiteScore}. Black wins!",
+ finalWhiteScore = action.whiteScore.toFloat(),
finalBlackScore = action.blackScore.toFloat(),
- aiWon = action.aiWon,
+ whiteWon = action.whiteWon,
previousButtonEnabled = true,
showAiEstimatedTerritory = false,
showFinalTerritory = true,
@@ -104,9 +130,9 @@ class AiGameReducer : Reducer {
aiAnalysis = action.aiAnalisis,
aiQuickEstimation = action.selectedMove,
chatText = when {
- newVariation.isGameOver() && state.aiWon == true -> "Game ended because of two passes. Final score is black ${state.finalBlackScore?.toInt()} to white ${state.finalWhiteScore}. Looks like I win this time."
- newVariation.isGameOver() && state.aiWon == false -> "Game ended because of two passes. Final score is black ${state.finalBlackScore?.toInt()} to white ${state.finalWhiteScore}. Congrats, looks like you got the better of me."
- newVariation.isGameOver() && state.aiWon == null -> "Game ended because of two passes. Hang on, I'm computing the final score."
+ newVariation.isGameOver() && state.whiteWon == true -> "Game ended because of two passes. Final score is black ${state.finalBlackScore?.toInt()} to white ${state.finalWhiteScore}. White wins!"
+ newVariation.isGameOver() && state.whiteWon == false -> "Game ended because of two passes. Final score is black ${state.finalBlackScore?.toInt()} to white ${state.finalWhiteScore}. Black wins!"
+ newVariation.isGameOver() && state.whiteWon == null -> "Game ended because of two passes. Hang on, I'm computing the final score."
else -> state.chatText
}
)
@@ -123,11 +149,18 @@ class AiGameReducer : Reducer {
passButtonEnabled = true,
previousButtonEnabled = state.history.size > 2,
chatText = when {
- state.engineStarted && state.position?.lastMove?.x == -1 -> "Pass! If you agree the game is over you should pass as well."
- state.position != null && state.engineStarted -> "Your turn!"
+ state.engineStarted && state.position?.lastMove?.x == -1 -> "Pass! Another pass will conclude the game."
+ state.engineStarted && state.position?.nextToMove == StoneType.WHITE -> "White's turn!"
+ state.engineStarted && state.position?.nextToMove == StoneType.BLACK -> "Black's turn!"
else -> state.chatText
}
)
+ NextPlayerChanged -> state.copy(
+ boardIsInteractive = !state.boardIsInteractive,
+ passButtonEnabled = !state.passButtonEnabled,
+ previousButtonEnabled = !state.previousButtonEnabled,
+ nextButtonEnabled = !state.nextButtonEnabled
+ )
is UserHotTrackedCoordinate -> state.copy(
candidateMove = action.coordinate
)
@@ -146,12 +179,15 @@ class AiGameReducer : Reducer {
chatText = "An error occurred communicating with the AI"
)
UserPressedPrevious -> {
- val newHistory = 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)
state.copy(
position = newHistory.lastOrNull(),
- redoPosStack = state.redoPosStack + state.history.takeLast(2),
+ redoPosStack = state.redoPosStack + removedHistory,
history = newHistory,
- previousButtonEnabled = newHistory.size > 2,
+ previousButtonEnabled = newHistory.size > 1,
showHints = false,
hintButtonVisible = true,
ownershipButtonVisible = true,
@@ -161,7 +197,7 @@ class AiGameReducer : Reducer {
boardIsInteractive = true,
passButtonEnabled = true,
chatText = "Ok, let's try again. Your turn!",
- aiWon = null,
+ whiteWon = null,
finalBlackScore = null,
finalWhiteScore = null
)
@@ -188,9 +224,10 @@ class AiGameReducer : Reducer {
boardSize = action.size,
handicap = action.handicap,
enginePlaysBlack = !action.youPlayBlack,
+ enginePlaysWhite = !action.youPlayWhite,
newGameDialogShown = false,
showHints = false,
- aiWon = null,
+ whiteWon = null,
finalWhiteScore = null,
finalBlackScore = null,
showFinalTerritory = false,
@@ -238,6 +275,12 @@ class AiGameReducer : Reducer {
chatText = "Ok, your turn",
boardIsInteractive = true
)
+ ToggleAIBlack -> state.copy(
+ enginePlaysBlack = !state.enginePlaysBlack
+ )
+ ToggleAIWhite -> state.copy(
+ enginePlaysWhite = !state.enginePlaysWhite
+ )
is EngineWouldNotStart -> state.copy(
boardIsInteractive = false,
hintButtonVisible = false,
@@ -246,4 +289,12 @@ class AiGameReducer : Reducer {
)
}
}
-}
\ No newline at end of file
+
+ private fun aiMovedLast(state: AiGameState): Boolean =
+ (state.position?.lastPlayerToMove == StoneType.BLACK && state.enginePlaysBlack) ||
+ (state.position?.lastPlayerToMove == StoneType.WHITE && state.enginePlaysWhite)
+
+
+ private fun aiOnlyGame(state: AiGameState): Boolean =
+ state.enginePlaysBlack || state.enginePlaysWhite
+}
diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/AiGameState.kt b/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/AiGameState.kt
index d0f3fd4c..0498443f 100644
--- a/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/AiGameState.kt
+++ b/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/AiGameState.kt
@@ -13,6 +13,7 @@ data class AiGameState(
val history: List = emptyList(),
val boardSize: Int = 19,
val enginePlaysBlack: Boolean = false,
+ val enginePlaysWhite: Boolean = false,
val handicap: Int = 0,
val boardIsInteractive: Boolean = false,
val candidateMove: Cell? = null,
@@ -29,8 +30,8 @@ data class AiGameState(
val ownershipButtonVisible: Boolean = false,
val finalWhiteScore: Float? = null,
val finalBlackScore: Float? = null,
- val aiWon: Boolean? = null,
+ val whiteWon: Boolean? = null,
val stateRestorePending: Boolean = true,
val aiAnalysis: Response? = null,
val aiQuickEstimation: MoveInfo? = null,
-)
\ No newline at end of file
+)
diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/NewGameBottomSheet.kt b/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/NewGameBottomSheet.kt
index c96017c4..b57ca6a7 100644
--- a/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/NewGameBottomSheet.kt
+++ b/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/NewGameBottomSheet.kt
@@ -9,10 +9,11 @@ import io.zenandroid.onlinego.R
import io.zenandroid.onlinego.databinding.BottomSheetNewAiGameBinding
private const val SIZE = "AI_GAME_SIZE"
-private const val COLOR = "AI_GAME_COLOR"
private const val HANDICAP = "AI_GAME_HANDICAP"
+private const val AI_BLACK = "AI_GAME_PLAY_BLACK"
+private const val AI_WHITE = "AI_GAME_PLAY_WHITE"
-class NewGameBottomSheet(context: Context, private val onOk: (Int, Boolean, Int) -> Unit) : BottomSheetDialog(context) {
+class NewGameBottomSheet(context: Context, private val onOk: (Int, Boolean, Boolean, Int) -> Unit, private val onLoad: () -> Unit, private val onSave: () -> Unit) : BottomSheetDialog(context) {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)!!
private lateinit var binding: BottomSheetNewAiGameBinding
@@ -32,9 +33,22 @@ class NewGameBottomSheet(context: Context, private val onOk: (Int, Boolean, Int)
else -> 19
}
val youPlayBlack = binding.blackButton.isChecked
+ val youPlayWhite = binding.whiteButton.isChecked
val handicap = binding.handicapSlider.value.toInt()
saveSettings()
- onOk.invoke(selectedSize, youPlayBlack, handicap)
+ onOk.invoke(selectedSize, youPlayBlack, youPlayWhite, handicap)
+ }
+
+ binding.loadButton.setOnClickListener {
+ dismiss()
+ saveSettings()
+ onLoad()
+ }
+
+ binding.saveButton.setOnClickListener {
+ dismiss()
+ saveSettings()
+ onSave()
}
binding.handicapSlider.addOnChangeListener { _, value, _ ->
@@ -68,12 +82,12 @@ class NewGameBottomSheet(context: Context, private val onOk: (Int, Boolean, Int)
}
binding.sizeToggleGroup.check(checkedSizeButtonId)
- val checkedColorButtonId = when(prefs.getInt(COLOR, 0)) {
- 0 -> R.id.blackButton
- 1 -> R.id.whiteButton
- else -> -1
+ if(prefs.getBoolean(AI_WHITE, false)) {
+ binding.colorToggleGroup.check(R.id.whiteButton)
+ }
+ if(prefs.getBoolean(AI_BLACK, false)) {
+ binding.colorToggleGroup.check(R.id.blackButton)
}
- binding.colorToggleGroup.check(checkedColorButtonId)
binding.handicapSlider.value = prefs.getFloat(HANDICAP, 0f)
setLabel(binding.handicapSlider.value)
@@ -87,9 +101,9 @@ class NewGameBottomSheet(context: Context, private val onOk: (Int, Boolean, Int)
}
prefs.edit()
.putInt(SIZE, selectedSize)
- .putInt(COLOR, if (binding.blackButton.isChecked) 0 else 1)
+ .putBoolean(AI_BLACK, binding.blackButton.isChecked)
+ .putBoolean(AI_WHITE, binding.whiteButton.isChecked)
.putFloat(HANDICAP, binding.handicapSlider.value)
.apply()
}
-
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/AIMoveMiddleware.kt b/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/AIMoveMiddleware.kt
index 6b2d03fb..4f26ae66 100644
--- a/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/AIMoveMiddleware.kt
+++ b/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/AIMoveMiddleware.kt
@@ -25,7 +25,7 @@ class AIMoveMiddleware : Middleware {
.flatMapSingle { (_, state) ->
KataGoAnalysisEngine.analyzeMoveSequence(
sequence = state.history,
- maxVisits = 20,
+ // maxVisits = 20,
komi = state.position?.komi ?: 0f,
includeOwnership = false,
includeMovesOwnership = false
@@ -33,8 +33,7 @@ class AIMoveMiddleware : Middleware {
.map {
val selectedMove = selectMove(it)
val move = Util.getCoordinatesFromGTP(selectedMove.move, state.position!!.boardHeight)
- val side = if(state.enginePlaysBlack) StoneType.BLACK else StoneType.WHITE
- val newPos = RulesManager.makeMove(state.position, side, move)
+ val newPos = RulesManager.makeMove(state.position, state.position.nextToMove, move)
if(newPos == null) {
recordException(Exception("KataGO wants to play move ${selectedMove.move} ($move), but RulesManager rejects it as invalid"))
AIError
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 c2b37634..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
@@ -1,6 +1,7 @@
package io.zenandroid.onlinego.ui.screens.localai.middlewares
import android.os.Bundle
+import android.util.Log
import androidx.core.os.bundleOf
import io.reactivex.Observable
import io.reactivex.rxkotlin.withLatestFrom
@@ -18,8 +19,9 @@ class AnalyticsMiddleware: Middleware {
override fun bind(actions: Observable, state: Observable): Observable {
return actions.withLatestFrom(state)
.doOnNext { (action, state) ->
+ Log.d("ai.state", "${action::class.simpleName}: ${action.toString()}")
when(action) {
- CantRestoreState, ViewReady, is RestoredState, ViewPaused, ShowNewGameDialog, DismissNewGameDialog, PromptUserForMove, is NewPosition, is UserHotTrackedCoordinate, is AIOwnershipResponse,
+ CantRestoreState, is ViewReady, is RestoredState, ViewPaused, ShowNewGameDialog, DismissNewGameDialog, PromptUserForMove, is NewPosition, is UserHotTrackedCoordinate, is AIOwnershipResponse,
HideOwnership -> Unit
is NewGame -> {
analytics.logEvent("ai_game_new_game", null)
@@ -35,7 +37,7 @@ class AnalyticsMiddleware: Middleware {
is AIMove -> analytics.logEvent("katago_move", null)
is AIHint -> analytics.logEvent("katago_hint", null)
- is ScoreComputed -> if(action.aiWon) {
+ is ScoreComputed -> if(action.whiteWon) {
analytics.logEvent("katago_won", Bundle().apply {
OnlineGoApplication.instance.get< UserSessionRepository>().uiConfig?.user?.ranking?.let {
putInt("RANKING", it)
@@ -60,9 +62,12 @@ 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() }
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/EngineLifecycleMiddleware.kt b/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/EngineLifecycleMiddleware.kt
index 21b1048c..f4838c43 100644
--- a/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/EngineLifecycleMiddleware.kt
+++ b/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/EngineLifecycleMiddleware.kt
@@ -21,7 +21,7 @@ class EngineLifecycleMiddleware : Middleware {
}
private fun startEngineObservable(actions: Observable, state: Observable): Observable =
- actions.ofType(ViewReady.javaClass)
+ actions.filter({ it is ViewReady })
.withLatestFrom(state)
.filter { (_, state) -> !state.engineStarted }
.flatMapSingle { (_, _) ->
@@ -48,4 +48,4 @@ class EngineLifecycleMiddleware : Middleware {
Log.e("EngineLifecycleMiddlew", throwable.message, throwable)
recordException(throwable)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/GameTurnMiddleware.kt b/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/GameTurnMiddleware.kt
index 4b14e0d6..ad668f7e 100644
--- a/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/GameTurnMiddleware.kt
+++ b/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/GameTurnMiddleware.kt
@@ -18,16 +18,19 @@ import io.zenandroid.onlinego.utils.recordException
class GameTurnMiddleware : Middleware {
override fun bind(actions: Observable, state: Observable): Observable =
Observable.merge(
+ listOf(
engineStarted(actions, state),
newGame(actions),
nextMove(actions, state),
- computeScore(actions, state)
+ computeScore(actions, state),
+ toggleAI(actions, state)
+ )
)
private fun engineStarted(actions: Observable, state: Observable) =
actions.filter { it is EngineStarted || it is RestoredState }
.withLatestFrom(state)
- .filter { (_, state) -> !state.stateRestorePending && state.engineStarted && state.position != null && !(state.history.isGameOver() && state.aiWon != null) }
+ .filter { (_, state) -> !state.stateRestorePending && state.engineStarted && state.position != null && !(state.history.isGameOver() && state.whiteWon != null) }
.map { (_, state) -> NewPosition(state.position!!) }
private fun newGame(actions: Observable) =
@@ -35,12 +38,12 @@ class GameTurnMiddleware : Middleware {
.map { NewPosition(RulesManager.initializePosition(it.size, it.handicap)) }
private fun nextMove(actions: Observable, state: Observable) =
- actions.filter { it is NewPosition || it is AIMove }
+ actions.filter { it is NewPosition || it is AIMove || it is NextPlayerChanged || it is DismissNewGameDialog }
.withLatestFrom(state)
- .filter { (_, state) -> !state.history.isGameOver() }
+ .filter { (_, state) -> !state.history.isGameOver() && !state.newGameDialogShown }
.map { (_, state) ->
- val isBlacksTurn = state.position?.nextToMove != StoneType.WHITE
- if (isBlacksTurn == state.enginePlaysBlack) {
+ if ((state.position?.nextToMove == StoneType.BLACK && state.enginePlaysBlack) ||
+ (state.position?.nextToMove == StoneType.WHITE && state.enginePlaysWhite)) {
GenerateAiMove
} else {
PromptUserForMove
@@ -54,7 +57,7 @@ class GameTurnMiddleware : Middleware {
.flatMap { (_, state) ->
KataGoAnalysisEngine.analyzeMoveSequence(
sequence = state.history,
- maxVisits = 10,
+ //maxVisits = 10,
komi = state.position!!.komi,
includeOwnership = true
)
@@ -90,8 +93,8 @@ class GameTurnMiddleware : Middleware {
val whiteScore = (newPos.komi ?: 0f) + newPos.whiteTerritory.size + newPos.whiteCaptureCount + newPos.blackDeadStones.size
val blackScore = newPos.blackTerritory.size + newPos.blackCaptureCount + newPos.whiteDeadStones.size
- val aiWon = state.enginePlaysBlack == (blackScore > whiteScore)
- ScoreComputed(newPos, whiteScore, blackScore, aiWon, it)
+ val whiteWon = blackScore < whiteScore
+ ScoreComputed(newPos, whiteScore, blackScore, whiteWon, it)
}
.subscribeOn(Schedulers.io())
.toObservable()
@@ -99,8 +102,19 @@ class GameTurnMiddleware : Middleware {
.onErrorResumeNext(Observable.empty())
}
+ private fun toggleAI(actions: Observable, state: Observable) =
+ actions.filter { it is ToggleAIBlack || it is ToggleAIWhite }
+ .withLatestFrom(state)
+ .filter{ (sideChanged, state) -> state.engineStarted && state.position != null
+ && state.position.nextToMove == when(sideChanged) {
+ ToggleAIBlack -> StoneType.BLACK
+ ToggleAIWhite -> StoneType.WHITE
+ else -> null
+ } }
+ .map { NextPlayerChanged }
+
private fun onError(throwable: Throwable) {
Log.e("GameTurnMiddleware", throwable.message, throwable)
recordException(throwable)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/HintMiddleware.kt b/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/HintMiddleware.kt
index d55c8c93..30a8b658 100644
--- a/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/HintMiddleware.kt
+++ b/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/HintMiddleware.kt
@@ -19,7 +19,7 @@ class HintMiddleware : Middleware {
.flatMap { (_, state) ->
KataGoAnalysisEngine.analyzeMoveSequence(
sequence = state.history,
- maxVisits = 30,
+ //maxVisits = 30,
komi = state.position?.komi ?: 0f,
includeOwnership = false
)
@@ -35,4 +35,4 @@ class HintMiddleware : Middleware {
Log.e("HintMiddleware", throwable.message, throwable)
recordException(throwable)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/OwnershipMiddleware.kt b/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/OwnershipMiddleware.kt
index 19c7a529..e1f53a68 100644
--- a/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/OwnershipMiddleware.kt
+++ b/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/OwnershipMiddleware.kt
@@ -20,7 +20,7 @@ class OwnershipMiddleware : Middleware {
} else {
KataGoAnalysisEngine.analyzeMoveSequence(
sequence = state.history,
- maxVisits = 30,
+ //maxVisits = 30,
komi = state.position!!.komi,
includeOwnership = true
).map {
@@ -30,4 +30,4 @@ class OwnershipMiddleware : Middleware {
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/StatePersistenceMiddleware.kt b/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/StatePersistenceMiddleware.kt
index c0ed0cee..10a1fa58 100644
--- a/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/StatePersistenceMiddleware.kt
+++ b/app/src/main/java/io/zenandroid/onlinego/ui/screens/localai/middlewares/StatePersistenceMiddleware.kt
@@ -44,8 +44,10 @@ class StatePersistenceMiddleware : Middleware {
private fun deserializeObservable(actions: Observable, state: Observable) =
actions.ofType(ViewReady::class.java)
.withLatestFrom(state)
- .map {
- if(prefs.contains(STATE_KEY)) {
+ .map { (_, state) ->
+ if(state.position != null) {
+ RestoredState(state)
+ } else if(prefs.contains(STATE_KEY)) {
val json = prefs.getString(STATE_KEY, "")!!
val newState = try {
stateAdapter.fromJson(json)
diff --git a/app/src/main/java/io/zenandroid/onlinego/ui/screens/main/MainActivity.kt b/app/src/main/java/io/zenandroid/onlinego/ui/screens/main/MainActivity.kt
index a00cb225..93af1cee 100644
--- a/app/src/main/java/io/zenandroid/onlinego/ui/screens/main/MainActivity.kt
+++ b/app/src/main/java/io/zenandroid/onlinego/ui/screens/main/MainActivity.kt
@@ -9,10 +9,12 @@ import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Color
+import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
+import android.util.Log
import android.view.MenuItem
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
@@ -127,6 +129,23 @@ class MainActivity : AppCompatActivity(), MainContract.View {
)
BoardView.preloadResources(resources)
+
+ intent?.data?.let { data ->
+ // Figure out what to do based on the intent type
+ if (intent?.scheme?.startsWith("http") == true) {
+ // Handle intents with remote data ...
+ Log.d("MainActivity", "Recieved remote intent ${data}")
+ navHostFragment.navController.navigate(R.id.aiGameFragment, Bundle().apply {
+ putString("SGF_REMOTE", data.toString())
+ })
+ } else if (intent?.type == "application/x-go-sgf") {
+ // Handle intents with local data ...
+ Log.d("MainActivity", "Recieved local intent ${data}")
+ navHostFragment.navController.navigate(R.id.aiGameFragment, Bundle().apply {
+ putString("SGF_LOCAL", data.toString())
+ })
+ }
+ }
}
private fun scheduleNotificationJob() {
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 5a2262b4..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
@@ -107,6 +112,7 @@ import io.zenandroid.onlinego.ui.screens.settings.SettingsAction.ThemeClicked
import io.zenandroid.onlinego.ui.theme.OnlineGoTheme
import io.zenandroid.onlinego.utils.processGravatarURL
import io.zenandroid.onlinego.utils.rememberStateWithLifecycle
+import it.sephiroth.android.library.numberpicker.*
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
@@ -406,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(
@@ -452,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(
@@ -487,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(
@@ -582,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 e1ff4e41..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
@@ -14,6 +14,8 @@ import android.view.View
import androidx.compose.ui.unit.dp
import androidx.core.graphics.ColorUtils
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
import io.zenandroid.onlinego.OnlineGoApplication
@@ -25,6 +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.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
@@ -37,6 +42,8 @@ import kotlin.math.roundToInt
* that is passed to it via setPosition()
*/
class BoardView : View {
+ private val settingsRepository: SettingsRepository = GlobalContext.get().get()
+
var boardWidth = 19
set(boardWidth) {
field = boardWidth
@@ -147,6 +154,7 @@ class BoardView : View {
}
field = value
}
+ var hintBasis: RootInfo? = null
var hints: List? = null
var ownership: List? = null
@@ -444,10 +452,23 @@ class BoardView : View {
private fun drawHints(canvas: Canvas, position: Position) {
hints?.let {
- for((index, hint) in it.take(5).withIndex()) {
+ val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
+ val adapter = moshi.adapter(RootInfo::class.java)
+ Log.d("BoardView", "Root: ${hintBasis?.let { adapter.toJson(it) }}")
+ for((index, hint) in it.withIndex()) {
+ 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(hintBasis?.scoreLead ?: 0f)
+ val scoreDiff = blackScoreDiff *
+ if(position.nextToMove == StoneType.WHITE) -1 else 1
+
val coords = Util.getCoordinatesFromGTP(hint.move, position.boardHeight)
val center = getCellCenter(coords.x, coords.y)
- val drawable = if (position.nextToMove == StoneType.BLACK) blackStoneDrawable else whiteStoneDrawable
+ val drawable = if (position.nextToMove == StoneType.WHITE || settingsRepository.detailedAnalysis) whiteStoneDrawable else blackStoneDrawable
drawable.alpha = 100
drawable.setBounds(
(center.x - cellSize / 2f + stoneSpacing).toInt(),
@@ -455,11 +476,33 @@ class BoardView : View {
(center.x + cellSize / 2f - stoneSpacing).toInt(),
(center.y + cellSize / 2f - stoneSpacing).toInt()
)
+ val rank = (hint.winrate - winrateLowest) / (winrateHighest - winrateLowest)
+ val red = if(rank > 0.5) 1 - 2 * rank else 1.0f
+ val green = if(rank > 0.5) 1.0f else 2 * rank
+ val colour = if(index == 0) Color.argb(1f, 0f, 1f, 1f)
+ else Color.argb(1f, red, green, 0f)
+ drawable.setColorFilter(colour, PorterDuff.Mode.MULTIPLY)
drawable.draw(canvas)
- textPaint.color = if (position.nextToMove == StoneType.WHITE) Color.BLACK else Color.WHITE
- drawTextCentred(canvas, textPaint, (index + 1).toString(), center.x, center.y)
+ if (settingsRepository.detailedAnalysis) {
+ val aiTextPaint = android.text.TextPaint(textPaint).also {
+ it.color = Color.BLACK
+ it.textSize = cellSize * .22f
+ }
+ 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}x ", center.x, center.y)
+ aiTextPaint.let {
+ it.setTypeface(Typeface.create(it.getTypeface(), Typeface.BOLD))
+ }
+ drawTextCentred(canvas, aiTextPaint, "${String.format("%.1f", winrate)}%", center.x, center.y + height)
+ } else {
+ textPaint.color = if (position.nextToMove == StoneType.WHITE) Color.BLACK else Color.WHITE
+ drawTextCentred(canvas, textPaint, "${index + 1}", center.x, center.y)
+ }
}
+ whiteStoneDrawable.clearColorFilter()
}
}
diff --git a/app/src/main/res/drawable/ic_branch_blue.xml b/app/src/main/res/drawable/ic_branch_blue.xml
new file mode 100644
index 00000000..0ed68b3e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_branch_blue.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
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/drawable/ic_thinking_blue.xml b/app/src/main/res/drawable/ic_thinking_blue.xml
new file mode 100644
index 00000000..faaa8da1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_thinking_blue.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/bottom_sheet_new_ai_game.xml b/app/src/main/res/layout/bottom_sheet_new_ai_game.xml
index 20e09ca7..a4276ba7 100644
--- a/app/src/main/res/layout/bottom_sheet_new_ai_game.xml
+++ b/app/src/main/res/layout/bottom_sheet_new_ai_game.xml
@@ -76,8 +76,8 @@
android:id="@+id/colorToggleGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- app:singleSelection="true"
- app:selectionRequired="true"
+ app:singleSelection="false"
+ app:selectionRequired="false"
app:checkedButton="@id/blackButton"
>
-
+ >
+
+
+
+
-
\ No newline at end of file
+
diff --git a/app/src/main/res/layout/fragment_aigame.xml b/app/src/main/res/layout/fragment_aigame.xml
index f495ea6f..a12e43ff 100644
--- a/app/src/main/res/layout/fragment_aigame.xml
+++ b/app/src/main/res/layout/fragment_aigame.xml
@@ -30,7 +30,7 @@
app:cardBackgroundColor="#FFFFFF"
app:cardCornerRadius="15dp"
app:cardElevation="2dp"
- app:layout_constraintBottom_toTopOf="@id/nameLabelLeft"
+ app:layout_constraintBottom_toTopOf="@id/nameButtonLeft"
app:layout_constraintDimensionRatio="h,1:1"
app:layout_constraintHeight_max="80dp"
app:layout_constraintLeft_toLeftOf="parent"
@@ -97,8 +97,8 @@
android:layout_marginRight="8dp"
/>
-
+ app:layout_constraintTop_toBottomOf="@id/iconContainerLeft"
+ style="@style/AIChatButton"
+ android:textColor="@color/colorTextSecondary"
+ android:paddingLeft="0dp"
+ android:paddingRight="0dp"
+ android:layout_marginRight="8dp"
+ />
+ app:layout_constraintTop_toTopOf="@id/nameButtonLeft" />
-
+ app:layout_constraintTop_toBottomOf="@id/iconContainerRight"
+ style="@style/AIChatButton"
+ android:textColor="@color/colorTextSecondary"
+ android:paddingLeft="0dp"
+ android:paddingRight="0dp"
+ android:layout_marginRight="8dp"
+ />
@@ -183,10 +195,10 @@
android:layout_height="wrap_content"
android:text="Prisoners"
android:textSize="12sp"
- app:layout_constraintBottom_toTopOf="@id/nameLabelLeft"
+ app:layout_constraintBottom_toTopOf="@id/nameButtonLeft"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
- app:layout_constraintTop_toTopOf="@id/nameLabelLeft" />
+ app:layout_constraintTop_toTopOf="@id/nameButtonLeft" />
-
\ No newline at end of file
+