Skip to content

Commit 6c9d62e

Browse files
Merge pull request #55 from a2i2/nick/interruptions-attempt-count
Add the attempt count & reset task logic [CON-2682]
2 parents 8ca00ee + d3635a8 commit 6c9d62e

File tree

12 files changed

+150
-34
lines changed

12 files changed

+150
-34
lines changed

EEFRT Demo Android/app/src/main/java/ai/a2i2/conductor/effrtdemoandroid/MainActivity.kt

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package ai.a2i2.conductor.effrtdemoandroid
22

33
import ai.a2i2.conductor.effrtdemoandroid.persistence.DatabaseProvider
44
import ai.a2i2.conductor.effrtdemoandroid.persistence.GameCache
5+
import ai.a2i2.conductor.effrtdemoandroid.persistence.GameStorage
56
import ai.a2i2.conductor.effrtdemoandroid.ui.EefrtScreen
67
import ai.a2i2.conductor.effrtdemoandroid.ui.EventLogsView
78
import ai.a2i2.conductor.effrtdemoandroid.ui.EefrtTrialDetailView
@@ -17,6 +18,7 @@ import androidx.compose.material3.Text
1718
import androidx.compose.runtime.Composable
1819
import androidx.compose.ui.Modifier
1920
import ai.a2i2.conductor.effrtdemoandroid.ui.theme.EFFRTDemoAndroidTheme
21+
import ai.a2i2.conductor.effrtdemoandroid.util.GameConfigUtils
2022
import androidx.compose.foundation.layout.Arrangement
2123
import androidx.compose.foundation.layout.Box
2224
import androidx.compose.foundation.layout.Column
@@ -76,7 +78,7 @@ fun NavigationController(eefrtScreenViewModel: EefrtScreenViewModel) {
7678
OptionsDialog(
7779
onDismissRequest = { showDialog.value = false },
7880
onOptionSelected = { option ->
79-
selectedOption.value = when(option) {
81+
selectedOption.value = when (option) {
8082
"Trial 2" -> "trial-seq-2.json"
8183
"Trial 3" -> "trial-seq-3.json"
8284
else -> "trial-seq-1.json" // default
@@ -93,6 +95,8 @@ fun NavigationController(eefrtScreenViewModel: EefrtScreenViewModel) {
9395
calibrationComplete = false
9496
)
9597
)
98+
eefrtScreenViewModel.updateGameMarkedAsComplete(context, false)
99+
eefrtScreenViewModel.updateEEFRTAttemptCount(context, 1)
96100
showDialog.value = false
97101
}
98102
)
@@ -118,6 +122,13 @@ fun NavigationController(eefrtScreenViewModel: EefrtScreenViewModel) {
118122
EefrtScreen(
119123
viewModel = eefrtScreenViewModel,
120124
onBack = {
125+
// check to see if the EEFRT attempt count is exceeded,
126+
// show on the home screen that the task would be marked as complete
127+
if (GameStorage(context).eefrtAttemptCount > GameConfigUtils.MAX_EEFRT_ATTEMPTS) {
128+
eefrtScreenViewModel.updateGameMarkedAsComplete(context, true)
129+
}
130+
131+
// close the view
121132
selectedOption.value = null
122133
navController.popBackStack()
123134
}
@@ -174,6 +185,8 @@ fun HomeScreen(
174185
onResumeTaskPressed: () -> Unit,
175186
onViewEventLogsPressed: () -> Unit,
176187
) {
188+
val context = LocalContext.current
189+
177190
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
178191
Column(
179192
horizontalAlignment = Alignment.CenterHorizontally,
@@ -199,6 +212,16 @@ fun HomeScreen(
199212
modifier = Modifier.padding(innerPadding),
200213
onClick = onViewEventLogsPressed
201214
)
215+
216+
if (BuildConfig.DEBUG) {
217+
Text("Current EEFRT attempt number: ${eefrtScreenViewModel.eefrtAttemptCount.value}")
218+
219+
Text("Current game marked as complete: ${eefrtScreenViewModel.gameMarkedAsComplete.value}")
220+
221+
if (eefrtScreenViewModel.resumeTrialAvailable.value) {
222+
Text("Can resume from: ${eefrtScreenViewModel.determineResumeTrialString(context)}")
223+
}
224+
}
202225
}
203226
}
204227
}

EEFRT Demo Android/app/src/main/java/ai/a2i2/conductor/effrtdemoandroid/persistence/GameStorage.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import hu.autsoft.krate.booleanPref
66
import hu.autsoft.krate.default.withDefault
77
import hu.autsoft.krate.intPref
88
import hu.autsoft.krate.kotlinx.kotlinxPref
9-
import hu.autsoft.krate.longPref
109
import kotlinx.serialization.Serializable
1110

1211
@Serializable
@@ -21,8 +20,8 @@ data class GameCache(
2120
var calibrationComplete: Boolean = false,
2221
var interruptionTimestamp: Long? = null
2322
) {
24-
fun isResumeTrialAvailable(): Boolean {
25-
return practiceComplete || trialNumber > 0
23+
fun isResumeTrialAvailable(context: Context): Boolean {
24+
return (practiceComplete || trialNumber > 0) && !GameStorage(context).gameMarkedAsComplete
2625
}
2726
}
2827

@@ -32,4 +31,6 @@ class GameStorage(context: Context) : SimpleKrate(context) {
3231
)
3332
var calibrationComplete: Boolean? by booleanPref("calibrationComplete").withDefault(null)
3433
var calibratedMaxPressCount: Int? by intPref("calibratedMaxPressCount").withDefault(null)
34+
var eefrtAttemptCount: Int by intPref("eefrtAttemptCount").withDefault(1)
35+
var gameMarkedAsComplete: Boolean by booleanPref("gameMarkedAsComplete").withDefault(false)
3536
}

EEFRT Demo Android/app/src/main/java/ai/a2i2/conductor/effrtdemoandroid/ui/EefrtScreen.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,13 @@ import android.os.Looper
1313
import android.util.Log
1414
import android.view.ViewGroup
1515
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
16-
import android.view.Window
1716
import android.webkit.WebResourceRequest
1817
import android.webkit.WebResourceResponse
1918
import android.webkit.WebView
2019
import androidx.compose.foundation.layout.Box
2120
import androidx.compose.foundation.layout.WindowInsets
2221
import androidx.compose.foundation.layout.asPaddingValues
23-
import androidx.compose.foundation.layout.padding
2422
import androidx.compose.foundation.layout.statusBars
25-
import androidx.compose.foundation.layout.systemBars
2623
import androidx.compose.material3.AlertDialog
2724
import androidx.compose.material3.Text
2825
import androidx.compose.material3.TextButton
@@ -32,12 +29,10 @@ import androidx.compose.runtime.MutableState
3229
import androidx.compose.runtime.mutableStateOf
3330
import androidx.compose.runtime.remember
3431
import androidx.compose.ui.Alignment
35-
import androidx.compose.ui.Modifier
3632
import androidx.compose.ui.platform.LocalContext
3733
import androidx.compose.ui.viewinterop.AndroidView
3834
import androidx.lifecycle.Lifecycle
3935
import androidx.lifecycle.LifecycleEventObserver
40-
import androidx.lifecycle.compose.LifecycleEventEffect
4136
import androidx.lifecycle.compose.LocalLifecycleOwner
4237
import androidx.webkit.WebViewAssetLoader
4338
import androidx.webkit.WebViewAssetLoader.AssetsPathHandler
@@ -166,7 +161,7 @@ fun EefrtScreen(
166161
confirmButton = {
167162
TextButton(
168163
onClick = {
169-
viewModel.onConfirmCloseDialog(TAG)
164+
viewModel.onConfirmCloseDialog(context, TAG)
170165
exitRequested.value = true
171166
dismiss(onBack)
172167
}
@@ -205,7 +200,7 @@ private fun onTaskResume(viewModel: EefrtScreenViewModel, context: Context, webV
205200
cache.interruptionTimestamp = interruptionTimestamp
206201

207202
// encode the cache and pass it along to the game
208-
val json = Json {
203+
val json = Json {
209204
encodeDefaults = true
210205
}
211206
val cacheJson = json.encodeToString(cache)
@@ -264,6 +259,8 @@ private fun handleMessage(
264259
TAG,
265260
"Incremented attempt count"
266261
)
262+
val currentAttemptCount = GameStorage(context).eefrtAttemptCount
263+
viewModel.updateEEFRTAttemptCount(context, currentAttemptCount + 1)
267264
}
268265

269266
// ensure the game data is reset if we've actually closed the task, similar scenario to the shouldShowExitDialog
@@ -272,6 +269,8 @@ private fun handleMessage(
272269
TAG,
273270
"Task will be restarted on next load"
274271
)
272+
viewModel.setCurrentGameState(context, null)
273+
viewModel.resumeTrialAvailable.value = false
275274
}
276275

277276
if (closeMessage.shouldShowExitDialog) {
@@ -321,6 +320,7 @@ private fun handleMessage(
321320

322321
"gameComplete" -> {
323322
viewModel.clearEEFRTData(context)
323+
viewModel.updateGameMarkedAsComplete(context, true)
324324
exitRequested.value = true
325325
dismiss(onBack)
326326
}

EEFRT Demo Android/app/src/main/java/ai/a2i2/conductor/effrtdemoandroid/ui/data/EefrtScreenViewModel.kt

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,16 @@ import ai.a2i2.conductor.effrtdemoandroid.persistence.GameCache
55
import ai.a2i2.conductor.effrtdemoandroid.persistence.GameStorage
66
import ai.a2i2.conductor.effrtdemoandroid.persistence.PracticeTaskAttempt
77
import ai.a2i2.conductor.effrtdemoandroid.persistence.TaskAttempt
8-
import ai.a2i2.conductor.effrtdemoandroid.ui.EefrtScreen
98
import ai.a2i2.conductor.effrtdemoandroid.util.GameConfigUtils
109
import android.content.Context
1110
import android.util.Log
1211
import androidx.compose.runtime.MutableState
1312
import androidx.compose.runtime.State
13+
import androidx.compose.runtime.mutableIntStateOf
1414
import androidx.compose.runtime.mutableStateOf
15-
import androidx.compose.ui.platform.LocalContext
1615
import androidx.lifecycle.ViewModel
1716
import androidx.lifecycle.viewModelScope
1817
import kotlinx.coroutines.launch
19-
import java.time.Instant
2018

2119
class EefrtScreenViewModel(
2220
private val appDatabase: AppDatabase,
@@ -31,10 +29,18 @@ class EefrtScreenViewModel(
3129
var interruptionTimestamp = mutableStateOf<Long?>(null)
3230
var closeMessage = mutableStateOf<CloseMessage?>(null)
3331

32+
var eefrtAttemptCount: MutableState<Int>
33+
var gameMarkedAsComplete: MutableState<Boolean>
34+
3435
init {
3536
refreshData()
36-
val currentGameState = GameStorage(context).cachedGameState
37-
resumeTrialAvailable = mutableStateOf(currentGameState?.isResumeTrialAvailable() ?: false)
37+
38+
val gameStorage = GameStorage(context)
39+
val currentGameState = gameStorage.cachedGameState
40+
resumeTrialAvailable =
41+
mutableStateOf(currentGameState?.isResumeTrialAvailable(context) ?: false)
42+
eefrtAttemptCount = mutableIntStateOf(gameStorage.eefrtAttemptCount)
43+
gameMarkedAsComplete = mutableStateOf(gameStorage.gameMarkedAsComplete)
3844
}
3945

4046
private fun refreshData() {
@@ -91,7 +97,7 @@ class EefrtScreenViewModel(
9197

9298
fun setCurrentGameState(context: Context, newGameState: GameCache?) {
9399
GameStorage(context).cachedGameState = newGameState
94-
resumeTrialAvailable.value = newGameState?.isResumeTrialAvailable() ?: false
100+
resumeTrialAvailable.value = newGameState?.isResumeTrialAvailable(context) ?: false
95101
}
96102

97103
fun getCurrentGameState(context: Context): GameCache? {
@@ -126,25 +132,48 @@ class EefrtScreenViewModel(
126132
showExitDialog.value = false
127133
}
128134

129-
fun onConfirmCloseDialog(loggingTag: String) {
130-
// No business logic for this app but a placeholder for the main apps
135+
fun onConfirmCloseDialog(context: Context, loggingTag: String) {
131136
closeMessage.value?.let {
132137
if (it.incrementAttemptCount) {
133138
Log.d(
134139
loggingTag,
135140
"Incremented attempt count"
136141
)
142+
val currentValue = GameStorage(context).eefrtAttemptCount
143+
updateEEFRTAttemptCount(context, currentValue + 1)
137144
}
138145

139146
if (it.taskRequiresRestart) {
140147
Log.d(
141148
loggingTag,
142149
"Task will be restarted on next load"
143150
)
151+
GameStorage(context).cachedGameState = null
152+
resumeTrialAvailable.value = false
144153
}
145154
}
146155

147156
// dismiss the dialog
148157
dismissCloseDialog()
149158
}
159+
160+
fun updateEEFRTAttemptCount(context: Context, newAttemptNumber: Int? = null) {
161+
GameStorage(context).eefrtAttemptCount =
162+
newAttemptNumber ?: 1 // if no value provided, then reset to the default value
163+
eefrtAttemptCount.value = newAttemptNumber ?: 1
164+
}
165+
166+
fun updateGameMarkedAsComplete(context: Context, newGameMarkedAsComplete: Boolean) {
167+
GameStorage(context).gameMarkedAsComplete = newGameMarkedAsComplete
168+
gameMarkedAsComplete.value = newGameMarkedAsComplete
169+
}
170+
171+
fun determineResumeTrialString(context: Context): String {
172+
val cache = GameStorage(context).cachedGameState
173+
return if (cache == null || !cache.practiceComplete) {
174+
"Tutorial"
175+
} else {
176+
"Trial ${cache.trialNumber + 1}" // remove the 0-indexing from the trial number
177+
}
178+
}
150179
}

EEFRT Demo Android/app/src/main/java/ai/a2i2/conductor/effrtdemoandroid/util/GameConfigUtils.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import android.content.Context
66
class GameConfigUtils {
77
companion object {
88
const val REWARD_PAYMENT_THRESHOLD = 0.8
9+
const val MAX_EEFRT_ATTEMPTS = 2
910

1011
fun rewardThresholdReached(context: Context): Boolean {
1112
val trialNumber = GameStorage(context).cachedGameState?.trialNumber?.plus(1) ?: 0

EEFRT Demo iOS/EEFRT Demo/ContentView.swift

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,33 @@ struct ContentView: View {
2323
Button("Cancel", role: .cancel) {}
2424
}
2525

26-
if let cache = viewModel.gameCache, cache.isResumeTrialAvailable() {
27-
NavigationLink(destination: EEFRTView(gameCache: cache)
28-
.ignoresSafeArea()
29-
.navigationBarBackButtonHidden()) {
26+
if let cache = viewModel.gameCache, cache.isResumeTrialAvailable() {
27+
NavigationLink(
28+
destination: EEFRTView(gameCache: cache)
29+
.ignoresSafeArea()
30+
.navigationBarBackButtonHidden()
31+
) {
3032
Text("Resume current EEFRT Task")
3133
}
3234
}
3335

3436
NavigationLink(destination: EventLogsView()) {
3537
Text("View Event Logs")
3638
}
39+
40+
#if DEBUG
41+
if let eefrtAttemptCount = viewModel.eefrtAttemptCount {
42+
Text("Current EEFRT attempt number: \(eefrtAttemptCount)")
43+
}
44+
45+
if let gameMarkedAsComplete = viewModel.gameMarkedAsComplete {
46+
Text("Current game marked as complete: \(gameMarkedAsComplete)")
47+
}
48+
49+
if let cache = Defaults.gameCache, cache.isResumeTrialAvailable() {
50+
Text("Can resume from: \(viewModel.determineTrialNumberStringFromCache())")
51+
}
52+
#endif
3753
}
3854
.padding()
3955
.navigationDestination(isPresented: $navigateToEEFRT) {
@@ -46,7 +62,7 @@ struct ContentView: View {
4662

4763
private func startTrial(number: Int) {
4864
var newGameCache = GameCache()
49-
switch(number) {
65+
switch number {
5066
case 2:
5167
newGameCache.trialSeqFilename = "trial-seq-2.json"
5268
case 3:
@@ -55,6 +71,8 @@ struct ContentView: View {
5571
newGameCache.trialSeqFilename = "trial-seq-1.json"
5672
}
5773
Defaults.gameCache = newGameCache
74+
Defaults.gameMarkedAsComplete = false
75+
Defaults.eefrtAttemptCount = 1
5876
navigateToEEFRT = true
5977
}
6078
}

EEFRT Demo iOS/EEFRT Demo/ContentViewModel.swift

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,37 @@ import SwiftyUserDefaults
55

66
class ContentViewModel: ObservableObject {
77
@Published var gameCache: GameCache? = nil
8+
@Published var gameMarkedAsComplete: Bool? = nil
9+
@Published var eefrtAttemptCount: Int? = nil
810

9-
private var defaultsObserver: DefaultsDisposable?
11+
private var defaultsGameCacheObserver: DefaultsDisposable?
12+
private var defaultsGameMarkedAsCompleteObserver: DefaultsDisposable?
13+
private var defaultsEefrtAttemptCountObserver: DefaultsDisposable?
1014

1115
init() {
1216
gameCache = Defaults.gameCache
13-
defaultsObserver = Defaults.observe(\.gameCache, options: [.new]) { [weak self] newCache in
17+
gameMarkedAsComplete = Defaults.gameMarkedAsComplete
18+
eefrtAttemptCount = Defaults.eefrtAttemptCount
19+
20+
defaultsGameCacheObserver = Defaults.observe(\.gameCache, options: [.new]) { [weak self] newCache in
1421
self?.gameCache = newCache.newValue?.map { $0 }
1522
}
23+
defaultsGameMarkedAsCompleteObserver = Defaults.observe(\.gameMarkedAsComplete, options: [.new]) { [weak self] gameMarkedAsComplete in
24+
self?.gameMarkedAsComplete = gameMarkedAsComplete.newValue.map { $0 }
25+
}
26+
defaultsEefrtAttemptCountObserver = Defaults.observe(\.eefrtAttemptCount, options: [.new]) { [weak self] eefrtAttemptCount in
27+
self?.eefrtAttemptCount = eefrtAttemptCount.newValue.map { $0 }
28+
}
1629
}
1730

1831
deinit {
19-
defaultsObserver?.dispose()
32+
defaultsGameCacheObserver?.dispose()
33+
defaultsGameMarkedAsCompleteObserver?.dispose()
34+
defaultsEefrtAttemptCountObserver?.dispose()
35+
}
36+
37+
func determineTrialNumberStringFromCache() -> String {
38+
guard let cache = Defaults.gameCache, cache.practiceComplete else { return "Tutorial" }
39+
return "Trial \(cache.trialNumber + 1)" // remove the 0 indexing from the trial number
2040
}
2141
}

0 commit comments

Comments
 (0)