diff --git a/app/src/main/assets/En/np_art.png b/app/src/main/assets/En/np_art.png new file mode 100644 index 000000000..fda684fea Binary files /dev/null and b/app/src/main/assets/En/np_art.png differ diff --git a/app/src/main/assets/En/np_buster.png b/app/src/main/assets/En/np_buster.png new file mode 100644 index 000000000..83662e29a Binary files /dev/null and b/app/src/main/assets/En/np_buster.png differ diff --git a/app/src/main/assets/En/np_quick.png b/app/src/main/assets/En/np_quick.png new file mode 100644 index 000000000..a3bb2fe65 Binary files /dev/null and b/app/src/main/assets/En/np_quick.png differ diff --git a/app/src/main/java/io/github/fate_grand_automata/di/script/PreferencesModule.kt b/app/src/main/java/io/github/fate_grand_automata/di/script/PreferencesModule.kt index cef7ac31f..ae6e981d1 100644 --- a/app/src/main/java/io/github/fate_grand_automata/di/script/PreferencesModule.kt +++ b/app/src/main/java/io/github/fate_grand_automata/di/script/PreferencesModule.kt @@ -7,6 +7,7 @@ import io.github.fate_grand_automata.scripts.models.AutoSkillCommand import io.github.fate_grand_automata.scripts.models.CardPriorityPerWave import io.github.fate_grand_automata.scripts.models.ServantPriorityPerWave import io.github.fate_grand_automata.scripts.models.SpamConfigPerTeamSlot +import io.github.fate_grand_automata.scripts.models.battle.ChainPriorityPerWave import io.github.fate_grand_automata.scripts.prefs.IBattleConfig import io.github.fate_grand_automata.scripts.prefs.IPreferences import io.github.fate_grand_automata.scripts.prefs.ISupportPreferences @@ -50,4 +51,9 @@ class PreferencesModule { @Provides fun provideServantPriority(battleConfig: IBattleConfig): ServantPriorityPerWave? = if (battleConfig.useServantPriority) battleConfig.servantPriority else null + + @ScriptScope + @Provides + fun provideChainPriority(battleConfig: IBattleConfig): ChainPriorityPerWave? = + if (battleConfig.useChainPriority) battleConfig.chainPriority else null } \ No newline at end of file diff --git a/app/src/main/java/io/github/fate_grand_automata/ui/Theme.kt b/app/src/main/java/io/github/fate_grand_automata/ui/Theme.kt index 67f307a38..af27aa2ab 100644 --- a/app/src/main/java/io/github/fate_grand_automata/ui/Theme.kt +++ b/app/src/main/java/io/github/fate_grand_automata/ui/Theme.kt @@ -159,6 +159,10 @@ private val DarkColors = darkColorScheme( fun FGAListItemColors() = ListItemDefaults.colors( containerColor = MaterialTheme.colorScheme.surfaceVariant ) +@Composable +fun FGAListItemColorsDisabled() = ListItemDefaults.colors( + containerColor = Color.Gray +) @Composable fun FGATheme( diff --git a/app/src/main/java/io/github/fate_grand_automata/ui/card_priority/CardPriorityListItem.kt b/app/src/main/java/io/github/fate_grand_automata/ui/card_priority/CardPriorityListItem.kt index 137e3cd41..b0c142258 100644 --- a/app/src/main/java/io/github/fate_grand_automata/ui/card_priority/CardPriorityListItem.kt +++ b/app/src/main/java/io/github/fate_grand_automata/ui/card_priority/CardPriorityListItem.kt @@ -2,12 +2,14 @@ package io.github.fate_grand_automata.ui.card_priority import androidx.compose.runtime.MutableState import io.github.fate_grand_automata.scripts.enums.BraveChainEnum +import io.github.fate_grand_automata.scripts.enums.ChainTypeEnum import io.github.fate_grand_automata.scripts.models.CardScore import io.github.fate_grand_automata.scripts.models.TeamSlot data class CardPriorityListItem( val scores: MutableList, val servantPriority: MutableList, + val chainPriority: MutableList, var rearrangeCards: MutableState, var braveChains: MutableState ) \ No newline at end of file diff --git a/app/src/main/java/io/github/fate_grand_automata/ui/card_priority/CardPriorityScreen.kt b/app/src/main/java/io/github/fate_grand_automata/ui/card_priority/CardPriorityScreen.kt index 44eb608cb..7c1c67337 100644 --- a/app/src/main/java/io/github/fate_grand_automata/ui/card_priority/CardPriorityScreen.kt +++ b/app/src/main/java/io/github/fate_grand_automata/ui/card_priority/CardPriorityScreen.kt @@ -10,7 +10,8 @@ fun CardPriorityScreen( ) { CardPriorityView( items = vm.cardPriorityItems, - useServantPriority = vm.useServantPriority + useServantPriority = vm.useServantPriority, + useChainPriority = vm.useChainPriority ) DisposableEffect(vm) { diff --git a/app/src/main/java/io/github/fate_grand_automata/ui/card_priority/CardPriorityView.kt b/app/src/main/java/io/github/fate_grand_automata/ui/card_priority/CardPriorityView.kt index a013599c9..e7db033b2 100644 --- a/app/src/main/java/io/github/fate_grand_automata/ui/card_priority/CardPriorityView.kt +++ b/app/src/main/java/io/github/fate_grand_automata/ui/card_priority/CardPriorityView.kt @@ -31,7 +31,8 @@ import kotlinx.coroutines.launch @Composable fun CardPriorityView( items: SnapshotStateList, - useServantPriority: Pref + useServantPriority: Pref, + useChainPriority: Pref ) { val pagerState = rememberPagerState(pageCount = {items.size},) val scope = rememberCoroutineScope() @@ -50,6 +51,13 @@ fun CardPriorityView( val servantPriority by useServantPriority.remember() useServantPriority.SwitchPreference( title = stringResource(R.string.p_battle_config_use_servant_priority), + modifier = Modifier + .padding(bottom = 4.dp) + ) + + val chainPriority by useChainPriority.remember() + useChainPriority.SwitchPreference( + title = stringResource(R.string.p_battle_config_use_chain_priority), modifier = Modifier .padding(bottom = 16.dp) ) @@ -81,7 +89,8 @@ fun CardPriorityView( } items.getOrNull(it)?.Render( - useServantPriority = servantPriority + useServantPriority = servantPriority, + useChainPriority = chainPriority ) } } diff --git a/app/src/main/java/io/github/fate_grand_automata/ui/card_priority/CardPriorityViewModel.kt b/app/src/main/java/io/github/fate_grand_automata/ui/card_priority/CardPriorityViewModel.kt index fcc987278..8bd3c1d6d 100644 --- a/app/src/main/java/io/github/fate_grand_automata/ui/card_priority/CardPriorityViewModel.kt +++ b/app/src/main/java/io/github/fate_grand_automata/ui/card_priority/CardPriorityViewModel.kt @@ -11,6 +11,7 @@ import io.github.fate_grand_automata.scripts.enums.BraveChainEnum import io.github.fate_grand_automata.scripts.models.CardPriority import io.github.fate_grand_automata.scripts.models.CardPriorityPerWave import io.github.fate_grand_automata.scripts.models.ServantPriorityPerWave +import io.github.fate_grand_automata.scripts.models.battle.ChainPriorityPerWave import javax.inject.Inject @HiltViewModel @@ -21,6 +22,7 @@ class CardPriorityViewModel @Inject constructor( val cardPriorityItems: SnapshotStateList by lazy { val cardPriority = battleConfig.cardPriority.get() val servantPriority = battleConfig.servantPriority.get() + val chainPriority = battleConfig.chainPriority.get() val rearrangeCards = battleConfig.rearrangeCards.get() val braveChains = battleConfig.braveChains.get() @@ -33,6 +35,7 @@ class CardPriorityViewModel @Inject constructor( CardPriorityListItem( it.value, servantPriority.atWave(it.index).toMutableList(), + chainPriority.atWave(it.index).toMutableList(), mutableStateOf(rearrangeCards.getOrElse(it.index) { false }), mutableStateOf(braveChains.getOrElse(it.index) { BraveChainEnum.None }) ) @@ -41,6 +44,7 @@ class CardPriorityViewModel @Inject constructor( } val useServantPriority = battleConfig.useServantPriority + val useChainPriority = battleConfig.useChainPriority fun save() { battleConfig.cardPriority.set( @@ -55,6 +59,12 @@ class CardPriorityViewModel @Inject constructor( ) ) + battleConfig.chainPriority.set( + ChainPriorityPerWave.from( + cardPriorityItems.map { it.chainPriority } + ) + ) + battleConfig.rearrangeCards.set(cardPriorityItems.map { it.rearrangeCards.value }) battleConfig.braveChains.set(cardPriorityItems.map { it.braveChains.value }) } diff --git a/app/src/main/java/io/github/fate_grand_automata/ui/card_priority/CardPriorityWaveSelector.kt b/app/src/main/java/io/github/fate_grand_automata/ui/card_priority/CardPriorityWaveSelector.kt index afad96631..e0320f4c9 100644 --- a/app/src/main/java/io/github/fate_grand_automata/ui/card_priority/CardPriorityWaveSelector.kt +++ b/app/src/main/java/io/github/fate_grand_automata/ui/card_priority/CardPriorityWaveSelector.kt @@ -108,6 +108,7 @@ fun CardPriorityWaveSelector( CardPriorityListItem( items[0].scores.toMutableList(), items[0].servantPriority.toMutableList(), + items[0].chainPriority.toMutableList(), mutableStateOf(false), mutableStateOf(BraveChainEnum.None) ) diff --git a/app/src/main/java/io/github/fate_grand_automata/ui/card_priority/Render.kt b/app/src/main/java/io/github/fate_grand_automata/ui/card_priority/Render.kt index 99e596029..f06ec2c01 100644 --- a/app/src/main/java/io/github/fate_grand_automata/ui/card_priority/Render.kt +++ b/app/src/main/java/io/github/fate_grand_automata/ui/card_priority/Render.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import io.github.fate_grand_automata.R import io.github.fate_grand_automata.scripts.enums.BraveChainEnum +import io.github.fate_grand_automata.scripts.enums.ChainTypeEnum import io.github.fate_grand_automata.scripts.models.TeamSlot import io.github.fate_grand_automata.ui.FGAListItemColors import io.github.fate_grand_automata.ui.drag_sort.DragSort @@ -30,7 +31,8 @@ import io.github.fate_grand_automata.util.stringRes @Composable fun CardPriorityListItem.Render( - useServantPriority: Boolean + useServantPriority: Boolean, + useChainPriority: Boolean ) { Column( horizontalAlignment = Alignment.CenterHorizontally @@ -53,7 +55,9 @@ fun CardPriorityListItem.Render( val braveChainDialog = listDialog( selected = braveChains, onSelectedChange = { braveChains = it }, - entries = BraveChainEnum.entries + entries = BraveChainEnum.entries.filter { + useChainPriority || it != BraveChainEnum.Always + } .associateWith { stringResource(it.stringRes) }, title = stringResource(R.string.p_brave_chains) ) @@ -61,7 +65,9 @@ fun CardPriorityListItem.Render( ListItem( modifier = Modifier .weight(1f) - .clickable { braveChainDialog.show() }, + .clickable { + braveChainDialog.show() + }, headlineContent = { Text(stringResource(R.string.p_brave_chains)) }, supportingContent = { Text(stringResource(braveChains.stringRes)) }, colors = FGAListItemColors() @@ -85,6 +91,12 @@ fun CardPriorityListItem.Render( } } + if (useChainPriority) { + ChainPriority( + priorities = chainPriority + ) + } + if (useServantPriority) { ServantPriority( priorities = servantPriority @@ -98,7 +110,7 @@ private fun ServantPriority( priorities: MutableList ) { Text( - "Servant Priority".uppercase(), + stringResource(R.string.p_servant_priority).uppercase(), modifier = Modifier .padding(bottom = 5.dp, top = 16.dp) ) @@ -122,4 +134,59 @@ private fun ServantPriority( ) } ) -} \ No newline at end of file +} + +@Composable +private fun ChainPriority( + priorities: MutableList +) { + Text( + stringResource(R.string.p_chain_priority).uppercase(), + modifier = Modifier + .padding(bottom = 10.dp, top = 10.dp) + ) + + val context = LocalContext.current + + val getForegroundColor = fun (enum: ChainTypeEnum): Int { + return (if (priorities.isAfterCutoff(enum)) context.getColor(R.color.colorDisabledText) + else when (enum) { + ChainTypeEnum.None -> Color.BLACK + else -> Color.WHITE + }) + } + val getBackgroundColor = fun (enum: ChainTypeEnum): Int { + return (if (priorities.isAfterCutoff(enum)) R.color.colorDisabledBackground + else when (enum) { + ChainTypeEnum.Arts -> R.color.colorArts + ChainTypeEnum.Quick -> R.color.colorQuick + ChainTypeEnum.Buster -> R.color.colorBuster + ChainTypeEnum.Mighty -> R.color.colorPrimaryDark + ChainTypeEnum.Avoid -> R.color.colorAvoid + ChainTypeEnum.None -> R.color.colorDisabledBackground + } + ).let { res -> context.getColor(res) } + } + val localizedStringMap: MutableMap = mutableMapOf() + ChainTypeEnum.entries.forEach { + localizedStringMap[it] = if (it == ChainTypeEnum.None) "│" else stringResource(it.stringRes) + } + DragSort( + items = priorities, + viewConfigGrabber = { + DragSortAdapter.ItemViewConfig( + foregroundColor = getForegroundColor(it), + backgroundColor = getBackgroundColor(it), + text = localizedStringMap.getOrElse(it) { it.name }, + ) + }, + updateBackgroundColorOnMove = true + ) +} + +fun MutableList.isAfterCutoff(chainTypeEnum: ChainTypeEnum, indexOfAvoid: Int? = null): Boolean { + val avoidIndex = indexOfAvoid ?: this.indexOf(ChainTypeEnum.Cutoff) + val enumIndex = this.indexOf(chainTypeEnum) + if (avoidIndex < 0) return false + return enumIndex > avoidIndex +} diff --git a/app/src/main/java/io/github/fate_grand_automata/ui/drag_sort/DragSort.kt b/app/src/main/java/io/github/fate_grand_automata/ui/drag_sort/DragSort.kt index 71b9da235..f9aee1b7f 100644 --- a/app/src/main/java/io/github/fate_grand_automata/ui/drag_sort/DragSort.kt +++ b/app/src/main/java/io/github/fate_grand_automata/ui/drag_sort/DragSort.kt @@ -10,7 +10,8 @@ import io.github.fate_grand_automata.util.ItemTouchHelperCallback @Composable fun DragSort( items: MutableList, - viewConfigGrabber: (T) -> DragSortAdapter.ItemViewConfig + viewConfigGrabber: (T) -> DragSortAdapter.ItemViewConfig, + updateBackgroundColorOnMove: Boolean = false ) { AndroidView( factory = { context -> @@ -23,9 +24,12 @@ fun DragSort( update = { it.adapter = DragSortAdapter( items, - viewConfigGrabber + viewConfigGrabber, + updateBackgroundColorOnMove ).also { adapter -> - val callback = ItemTouchHelperCallback(adapter) + val callback = ItemTouchHelperCallback( + adapter = adapter + ) val itemTouchHelper = ItemTouchHelper(callback) itemTouchHelper.attachToRecyclerView(it) diff --git a/app/src/main/java/io/github/fate_grand_automata/ui/drag_sort/DragSortAdapter.kt b/app/src/main/java/io/github/fate_grand_automata/ui/drag_sort/DragSortAdapter.kt index f8cd8f055..2285d5a96 100644 --- a/app/src/main/java/io/github/fate_grand_automata/ui/drag_sort/DragSortAdapter.kt +++ b/app/src/main/java/io/github/fate_grand_automata/ui/drag_sort/DragSortAdapter.kt @@ -15,7 +15,8 @@ import io.github.fate_grand_automata.util.IItemTouchHelperViewHolder class DragSortAdapter( private val items: MutableList, - private val viewConfigGrabber: (T) -> ItemViewConfig + private val viewConfigGrabber: (T) -> ItemViewConfig, + val updateBackgroundColorOnMove: Boolean = false, ) : RecyclerView.Adapter(), IItemTouchHelperAdapter { class ItemViewConfig( @ColorInt val foregroundColor: Int, @@ -59,11 +60,24 @@ class DragSortAdapter( holder.textView.setTextColor(viewConfig.foregroundColor) } - override fun onItemMove(From: Int, To: Int) { - val temp = items[From] - items[From] = items[To] - items[To] = temp + override fun onItemMove(from: Int, to: Int, origin: View?, target: View?) { + val temp = items[from] + items[from] = items[to] + items[to] = temp - notifyItemMoved(From, To) + // Swap views since the items also swapped + val views = listOf(target, origin) + for ((index, pos) in listOf(from, to).withIndex()) { + val currentItem = items[pos] + val viewConfig = viewConfigGrabber(currentItem) + val view = views[index] + if (updateBackgroundColorOnMove && view != null) { + view.setBackgroundColor(viewConfig.backgroundColor) + val textView: TextView = view.findViewById(R.id.drag_sort_text) + textView.setTextColor(viewConfig.foregroundColor) + } + } + + notifyItemMoved(from, to) } } \ No newline at end of file diff --git a/app/src/main/java/io/github/fate_grand_automata/ui/fine_tune/FineTuneSettingsViewModel.kt b/app/src/main/java/io/github/fate_grand_automata/ui/fine_tune/FineTuneSettingsViewModel.kt index c68e2b722..74efeec6e 100644 --- a/app/src/main/java/io/github/fate_grand_automata/ui/fine_tune/FineTuneSettingsViewModel.kt +++ b/app/src/main/java/io/github/fate_grand_automata/ui/fine_tune/FineTuneSettingsViewModel.kt @@ -59,7 +59,23 @@ class FineTuneSettingsViewModel @Inject constructor( valueRange = 50..100, valueRepresentation = { "$it%" }, hint = "Similarity threshold for detecting wave change. If your skill commands are used in the wrong wave, tweaking this might help." - ) + ), + FineTuneItem( + pref = prefs.npCardTypeSimilarity, + name = R.string.p_fine_tune_np_card_type_similarity, + icon = icon(Icons.Default.Star), + valueRange = 30..100, + valueRepresentation = { "$it%" }, + hint = "Similarity threshold used for the NP card type detection (Buster, Arts, Quick). Only used when Chain Priority is on." + ), + FineTuneItem( + pref = prefs.npSpamCardDetectionSimilarity, + name = R.string.p_fine_tune_np_spam_card_detection_similarity, + icon = icon(Icons.Default.Star), + valueRange = 30..100, + valueRepresentation = { "$it%" }, + hint = "Similarity threshold used for automatically detecting NPs in Spam mode. Only used when Chain Priority is on." + ), ) ), FineTuneGroup( @@ -135,10 +151,18 @@ class FineTuneSettingsViewModel @Inject constructor( pref = prefs.waitBeforeTurn, name = R.string.p_fine_tune_wait_before_turn, icon = icon(R.drawable.ic_time), - valueRange = 0..2000, + valueRange = 0..5000, valueRepresentation = { "${it}ms" }, hint = "Delay before the skill sequence starts after Battle screen is detected. Slower devices might need longer delay." ), + FineTuneItem( + pref = prefs.waitAfterOrderChange, + name = R.string.p_fine_tune_wait_after_order_change, + icon = icon(R.drawable.ic_time), + valueRange = 0..5000, + valueRepresentation = { "${it}ms" }, + hint = "Delay before the skill sequence starts after Order Change has been used. Slower devices might need longer delay." + ), FineTuneItem( pref = prefs.waitBeforeCards, name = R.string.p_fine_tune_wait_before_cards, diff --git a/app/src/main/java/io/github/fate_grand_automata/util/ItemTouchHelperCallback.kt b/app/src/main/java/io/github/fate_grand_automata/util/ItemTouchHelperCallback.kt index c3ca77089..3b9a1c183 100644 --- a/app/src/main/java/io/github/fate_grand_automata/util/ItemTouchHelperCallback.kt +++ b/app/src/main/java/io/github/fate_grand_automata/util/ItemTouchHelperCallback.kt @@ -3,11 +3,13 @@ package io.github.fate_grand_automata.util import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView -class ItemTouchHelperCallback(private val Adapter: IItemTouchHelperAdapter, - private val DragFlags: Int = ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) : ItemTouchHelper.Callback() { +class ItemTouchHelperCallback( + private val adapter: IItemTouchHelperAdapter, + private val dragFlags: Int = ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT, +) : ItemTouchHelper.Callback() { override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { - return makeMovementFlags(DragFlags, 0) + return makeMovementFlags(dragFlags, 0) } override fun isLongPressDragEnabled() = true @@ -15,7 +17,12 @@ class ItemTouchHelperCallback(private val Adapter: IItemTouchHelperAdapter, override fun isItemViewSwipeEnabled() = false override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { - Adapter.onItemMove(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition) + adapter.onItemMove( + viewHolder.bindingAdapterPosition, + target.bindingAdapterPosition, + viewHolder.itemView, + target.itemView + ) return true } diff --git a/app/src/main/java/io/github/fate_grand_automata/util/ItemTouchHelpers.kt b/app/src/main/java/io/github/fate_grand_automata/util/ItemTouchHelpers.kt index c8690a900..46dceb19c 100644 --- a/app/src/main/java/io/github/fate_grand_automata/util/ItemTouchHelpers.kt +++ b/app/src/main/java/io/github/fate_grand_automata/util/ItemTouchHelpers.kt @@ -1,7 +1,9 @@ package io.github.fate_grand_automata.util +import android.view.View + interface IItemTouchHelperAdapter { - fun onItemMove(From: Int, To: Int) + fun onItemMove(from: Int, to: Int, origin: View? = null, target: View? = null) } interface IItemTouchHelperViewHolder { diff --git a/app/src/main/java/io/github/fate_grand_automata/util/LocalizationExtensions.kt b/app/src/main/java/io/github/fate_grand_automata/util/LocalizationExtensions.kt index 246cbeeb2..2a4b99ca9 100644 --- a/app/src/main/java/io/github/fate_grand_automata/util/LocalizationExtensions.kt +++ b/app/src/main/java/io/github/fate_grand_automata/util/LocalizationExtensions.kt @@ -4,6 +4,7 @@ import io.github.fate_grand_automata.R import io.github.fate_grand_automata.prefs.core.GameAreaMode import io.github.fate_grand_automata.scripts.enums.BondCEEffectEnum import io.github.fate_grand_automata.scripts.enums.BraveChainEnum +import io.github.fate_grand_automata.scripts.enums.ChainTypeEnum import io.github.fate_grand_automata.scripts.enums.GameServer import io.github.fate_grand_automata.scripts.enums.MaterialEnum import io.github.fate_grand_automata.scripts.enums.RefillResourceEnum @@ -25,9 +26,20 @@ val BraveChainEnum.stringRes get() = when (this) { BraveChainEnum.None -> R.string.p_brave_chains_don_t_care BraveChainEnum.WithNP -> R.string.p_brave_chains_with_np + BraveChainEnum.Always -> R.string.p_brave_chains_always BraveChainEnum.Avoid -> R.string.p_brave_chains_avoid } +val ChainTypeEnum.stringRes + get() = when (this) { + ChainTypeEnum.Arts -> R.string.p_chain_type_arts + ChainTypeEnum.Quick -> R.string.p_chain_type_quick + ChainTypeEnum.Buster -> R.string.p_chain_type_buster + ChainTypeEnum.Mighty -> R.string.p_chain_type_mighty + ChainTypeEnum.Avoid -> R.string.p_chain_type_avoid + ChainTypeEnum.None -> R.string.p_spam_none + } + val SpamEnum.stringRes get() = when (this) { SpamEnum.None -> R.string.p_spam_none diff --git a/app/src/main/res/values-b+zh+CN/localized.xml b/app/src/main/res/values-b+zh+CN/localized.xml index c321a16be..f437ce63d 100644 --- a/app/src/main/res/values-b+zh+CN/localized.xml +++ b/app/src/main/res/values-b+zh+CN/localized.xml @@ -96,6 +96,7 @@ "区服" "指令卡优先级" "使用从者优先级" + "使用追击连携先级" "助战从者" "助战从者选取方式" "备用选取方式" @@ -128,6 +129,8 @@ "保存未修改的截图" "保存未修改的全屏截图,而非裁剪和重新缩放的截图" "加速道具" + "从者优先级" + "追击连携优先级" "追击连携" "调换顺序(优先级高的卡后出)" "跳过从者头像检查" @@ -163,6 +166,8 @@ "全局识别阈值(%)" "满破星识别阈值(%)" "战斗场次识别阈值(%)" + "宝具类型识别阈值(%)" + "宝具瞎打模式识别阈值(%)" "点击" "点击后暂停(ms)" "点击时间(ms)" @@ -174,6 +179,7 @@ "等待时间" "施放技能后等待时间(ms)" "回合间等待时间(ms)" + "换人(Order Change)后等待时间(ms)" "选择指令卡前等待时间(ms)" "等待时间加乘(%)" "FGA有问题请先点这(GitHub链接)" @@ -186,7 +192,13 @@ "圣晶石" "无偏好" "宝具施放后" + "始终追求链接" "避免追击连携" + "迅击" + "技击" + "力击" + "Mighty" + "避免" "关闭" "跳过" "瞎打模式" diff --git a/app/src/main/res/values-b+zh+TW/localized.xml b/app/src/main/res/values-b+zh+TW/localized.xml index 4e0120085..353d2fd2b 100644 --- a/app/src/main/res/values-b+zh+TW/localized.xml +++ b/app/src/main/res/values-b+zh+TW/localized.xml @@ -125,6 +125,8 @@ Order Change" "儲存原始截圖" "儲存截圖時,保留原始截圖大小而不進行影像縮放" "加速道具" + "偏好從者" + "偏好Chain" "Brave Chain" "調換順序 (優先性高的卡後出)" @@ -159,6 +161,8 @@ Order Change" "最低相似度(%)" "滿破相似度(%)" "關卡計數相似度(%)" + "Buster/Arts/Quick寶具施相似度(%)" + "自動模式寶具施相似度(%)" "點選" "點選後暫停(ms)" "點選時間(ms)" @@ -170,6 +174,7 @@ Order Change" "等待時間" "施放技能後等待時間(ms)" "回合間等待時間(ms)" + "换人(Order Change)后等待时间(ms)" "選擇指令卡前等待時間(ms)" "等待時間加乘(%)" "若 FGA 有問題請先參考此處(GitHub連結)" @@ -184,7 +189,13 @@ Order Change" "聖晶石" "無偏好" "寶具施放後" + "始终Brave Chain" "避免Brave Chain" + "Quick" + "Arts" + "Buster" + "Mighty" + "避免" "關閉" "跳過" "自動模式" @@ -396,4 +407,5 @@ Order Change" "金種火最大領取數(總枚數)" "更新已下載完成,於 5 秒鐘後重啟" "偏好從者" + "偏好Chain" diff --git a/app/src/main/res/values-ja/localized.xml b/app/src/main/res/values-ja/localized.xml index 0cb25c07a..9c40a2e2f 100644 --- a/app/src/main/res/values-ja/localized.xml +++ b/app/src/main/res/values-ja/localized.xml @@ -128,6 +128,8 @@ "未編集のスクリーンショットを保存" "トリミングや拡縮されたスクリーンショットの代わりに、未編集でフルサイズのスクリーンショットを保存します" "ブーストアイテム" + "サーヴァントの優先順位" + "チェインの優先順位" "Brave Chains" "優先順位を逆転する" "サーヴァント画像チェックをスキップ" @@ -163,6 +165,8 @@ "最小の類似度" "MLBの類似度" "ウェーブカウンターの類似度" + "宝具Buster/Arts/Quickの類似度" + "リピートモード宝具の類似度" "クリック" "クリック後の待機秒数" "クリックの継続時間" @@ -174,6 +178,7 @@ "待機" "スキル使用後の待機時間" "ターン経過後の待機時間" + "オーダーチェンジの後の待機時間" "カード選択後の待機時間" "待機時間倍率" "Troubleshooting" @@ -188,7 +193,13 @@ "聖晶石" "考慮しない" "宝具と同時に" + "いつも使用" "使用しない" + "Quick" + "Arts" + "Buster" + "マイティ" + "おわり" "使用確認OFF" "使用しない" "リピートモード" @@ -413,5 +424,6 @@ "10個消費する" "サーヴァントの優先順位を設定する" + "チェインの優先順位を設定する" \ No newline at end of file diff --git a/app/src/main/res/values-ko/localized.xml b/app/src/main/res/values-ko/localized.xml index 9460afab4..8e2187ce9 100644 --- a/app/src/main/res/values-ko/localized.xml +++ b/app/src/main/res/values-ko/localized.xml @@ -125,6 +125,8 @@ "스크린샷을 수정하지 않고 저장" "스크린샷을 저장할때 배율조정과 잘라내기가 적용되지 않은 전체화면 스크린샷을 사용" "부스트 아이템" + "서번트 우선순위" + "체인 우선순위" "브레이브 체인" "카드 재배열" "서번트 카드 아이콘 확인 생략" @@ -160,6 +162,8 @@ "최소 유사도 (%)" "최대해방 / 풀돌 유사도 (%)" "스테이지 카운터 유사도 (%)" + "보구랑 Buster/Arts/Quick 유사도 (%)" + "반복 모드 보구랑 유사도 (%)" "터치" "터치 후 딜레이 (ms)" "터치 지속 시간 (ms)" @@ -171,6 +175,7 @@ "딜레이" "스킬 딜레이 (ms)" "턴 시작 전 딜레이 (ms)" + "오더 체인지 후 딜레이 (ms)" "카드 뜨기 전 딜레이 (ms)" "딜레이 배율기 (%)" "문제 해결" @@ -183,7 +188,13 @@ "성정석" "상관 없음" "보구랑 같이" + "항상" "자제" + "Quick" + "Arts" + "Buster" + "Mighty" + "자제" "비활성화" "스킵" "반복 모드" @@ -393,6 +404,7 @@ "종화 획득 총량" "다운로드가 완료되었습니다. 5초 내로 재시작됩니다." "서번트 우선순위 사용" + "체인 우선순위 사용" "모두 초기화" "기본값으로 초기화" "사용할 언어를 선택하세요" diff --git a/app/src/main/res/values-vi/localized.xml b/app/src/main/res/values-vi/localized.xml index ca11704d0..475ee2011 100644 --- a/app/src/main/res/values-vi/localized.xml +++ b/app/src/main/res/values-vi/localized.xml @@ -124,6 +124,8 @@ Master" "Lưu ảnh chụp màn hình chưa sửa đổi" "Lưu ảnh chụp màn hình toàn màn hình chưa sửa đổi thay vì ảnh chụp màn hình bị cắt và thay đổi kích thước" "Vật phẩm tăng cường" + "Ưu tiên Servant" + "Ưu tiên Chain" "Brave Chains" "Sắp xếp lại thẻ" "Bỏ qua việc kiểm tra thẻ Servant" @@ -159,6 +161,8 @@ Sao chép hoặc đổi tên nó nếu bạn muốn giữ nó." "Độ tương tự tối thiểu" "MLB giống nhau" "Sự tương đồng của bộ đếm giai đoạn" + "NP Buster/Arts/Quick tương tự" + "Sự tương đồng trong phát hiện thư rác NP" "Số nhấp chuột" "Chờ sau khi nhấp chuột" "Thời lượng nhấp chuột" @@ -170,6 +174,7 @@ Sao chép hoặc đổi tên nó nếu bạn muốn giữ nó." "Chờ đợi" "Độ trễ kỹ năng" "Đợi trước khi rẽ" + "Đợi sau khi thay đổi thứ tự" "Chờ trước Thẻ" "Chờ nhân" "Xử lý sự cố" @@ -184,7 +189,13 @@ Sao chép hoặc đổi tên nó nếu bạn muốn giữ nó." "SQ" "không quan tâm" "Với NP" + "Luôn luôn" "Tránh" + "Quick" + "Arts" + "Buster" + "Mighty" + "Tránh" "Tắt" "Bỏ qua" "Chế độ spam" @@ -394,4 +405,5 @@ Sau khi nhấn nút, hãy chuyển bộ lọc ứng dụng từ \"Không đượ "Tổng số lượng than hồng vàng tối đa" "Tải xuống thành công. Khởi động lại ứng dụng sau 5 giây." "Sử dụng ưu tiên Servant" + "Sử dụng ưu tiên Chain" \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 7c509bb40..312d6a4c2 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -2,6 +2,8 @@ #1B3147 #3498db + #efefef + #909090 #2e7d32 #006064 @@ -22,6 +24,8 @@ #2e7d32 #7cb342 + #808080 + #B3FFFFFF #30000000 diff --git a/app/src/main/res/values/localized.xml b/app/src/main/res/values/localized.xml index 75aacc6b7..d63cd2440 100644 --- a/app/src/main/res/values/localized.xml +++ b/app/src/main/res/values/localized.xml @@ -96,6 +96,7 @@ Skills" "Server" "Card Priority" "Use Servant Priority" + "Use Chain Priority" "Support" "Selection Mode" "Fallback" @@ -129,6 +130,8 @@ Skills" "Save unmodified screenshots" "Save unmodified full-screen screenshots instead of cropped and rescaled screenshots" "Boost Item" + "Servant Priority" + "Chain Priority" "Brave Chains" "Rearrange Cards" "Skip servant face-card checks" @@ -164,6 +167,8 @@ Copy or rename it if you want to keep it." "Min Similarity" "MLB Similarity" "Stage Counter Similarity" + "NP Card Type Similarity" + "NP Spam Detection Similarity" "Clicks" "Wait after clicking" "Click duration" @@ -175,6 +180,7 @@ Copy or rename it if you want to keep it." "Wait" "Skill Delay" "Wait before Turn" + "Wait after Order Change" "Wait before Cards" "Wait Multiplier" "Troubleshooting" @@ -187,7 +193,13 @@ Copy or rename it if you want to keep it." "SQ" "Don't care" "With NP" + "Always" "Avoid" + "Quick" + "Arts" + "Buster" + "Mighty" + "Avoid" "Disabled" "Skip" "Spam mode" diff --git a/libautomata/src/main/java/io/github/lib_automata/AutomataApi.kt b/libautomata/src/main/java/io/github/lib_automata/AutomataApi.kt index 6e768a3df..b41bff33f 100644 --- a/libautomata/src/main/java/io/github/lib_automata/AutomataApi.kt +++ b/libautomata/src/main/java/io/github/lib_automata/AutomataApi.kt @@ -39,6 +39,11 @@ interface AutomataApi { similarity: Double? = null ): Match? = patterns.firstNotNullOfOrNull { find(it, similarity) } + fun Pattern.find( + pattern: Pattern, + similarity: Double? = null + ): Match? = this.findMatches(pattern, similarity ?: 0.0).firstOrNull() + fun Region.exists( image: Pattern, timeout: Duration = Duration.ZERO, diff --git a/prefs/src/main/java/io/github/fate_grand_automata/prefs/BattleConfig.kt b/prefs/src/main/java/io/github/fate_grand_automata/prefs/BattleConfig.kt index e6ae50cdd..978104f23 100644 --- a/prefs/src/main/java/io/github/fate_grand_automata/prefs/BattleConfig.kt +++ b/prefs/src/main/java/io/github/fate_grand_automata/prefs/BattleConfig.kt @@ -21,6 +21,9 @@ internal class BattleConfig( override val servantPriority by prefs.servantPriority override val useServantPriority by prefs.useServantPriority + override val chainPriority by prefs.chainPriority + override val useChainPriority by prefs.useChainPriority + override val party by prefs.party override val materials by prefs.materials diff --git a/prefs/src/main/java/io/github/fate_grand_automata/prefs/Preferences.kt b/prefs/src/main/java/io/github/fate_grand_automata/prefs/Preferences.kt index bcee9eecd..7a39b02a1 100644 --- a/prefs/src/main/java/io/github/fate_grand_automata/prefs/Preferences.kt +++ b/prefs/src/main/java/io/github/fate_grand_automata/prefs/Preferences.kt @@ -97,6 +97,12 @@ class PreferencesImpl @Inject constructor( override val waitBeforeCards by prefs.waitBeforeCards.map { it.milliseconds } + override val waitAfterOrderChange by prefs.waitAfterOrderChange.map { it.milliseconds } + + override val npSpamCardDetectionSimilarity by prefs.npSpamCardDetectionSimilarity.map { it / 100.0 } + + override val npCardTypeSimilarity by prefs.npCardTypeSimilarity.map { it / 100.0 } + override var maxGoldEmberStackSize by prefs.maxGoldEmberSetSize override var maxGoldEmberTotalCount by prefs.maxGoldEmberTotalCount diff --git a/prefs/src/main/java/io/github/fate_grand_automata/prefs/core/BattleConfigCore.kt b/prefs/src/main/java/io/github/fate_grand_automata/prefs/core/BattleConfigCore.kt index c5e528083..f567690e4 100644 --- a/prefs/src/main/java/io/github/fate_grand_automata/prefs/core/BattleConfigCore.kt +++ b/prefs/src/main/java/io/github/fate_grand_automata/prefs/core/BattleConfigCore.kt @@ -14,6 +14,7 @@ import io.github.fate_grand_automata.scripts.enums.ShuffleCardsEnum import io.github.fate_grand_automata.scripts.models.CardPriorityPerWave import io.github.fate_grand_automata.scripts.models.ServantPriorityPerWave import io.github.fate_grand_automata.scripts.models.ServantSpamConfig +import io.github.fate_grand_automata.scripts.models.battle.ChainPriorityPerWave class BattleConfigCore( val id: String, @@ -111,6 +112,19 @@ class BattleConfigCore( default = ServantPriorityPerWave.default ) + val useChainPriority = maker.bool("use_chain_priority") + val chainPriority = maker.serialized( + "chain_priority", + serializer = object : Serializer { + override fun deserialize(serialized: String): ChainPriorityPerWave = + ChainPriorityPerWave.of(serialized) + + override fun serialize(value: ChainPriorityPerWave) = + value.toString() + }, + default = ChainPriorityPerWave.default + ) + private val gson = Gson() private val defaultSpamConfig = (1..6).map { ServantSpamConfig() } diff --git a/prefs/src/main/java/io/github/fate_grand_automata/prefs/core/PrefsCore.kt b/prefs/src/main/java/io/github/fate_grand_automata/prefs/core/PrefsCore.kt index 091ddcab7..0af296604 100644 --- a/prefs/src/main/java/io/github/fate_grand_automata/prefs/core/PrefsCore.kt +++ b/prefs/src/main/java/io/github/fate_grand_automata/prefs/core/PrefsCore.kt @@ -61,12 +61,15 @@ class PrefsCore @Inject constructor( val minSimilarity = maker.int("min_similarity", 80) val mlbSimilarity = maker.int("mlb_similarity", 70) val stageCounterSimilarity = maker.int("stage_counter_similarity", 85) + val npCardTypeSimilarity = maker.int("card_type_similarity", 50) + val npSpamCardDetectionSimilarity = maker.int("np_card_detection_similarity", 80) val stageCounterNew = maker.bool("stage_counter_new") val skillDelay = maker.int("skill_delay", 500) val waitMultiplier = maker.int("wait_multiplier", 100) val waitBeforeTurn = maker.int("wait_before_turn", 500) val waitBeforeCards = maker.int("wait_before_cards", 2000) + val waitAfterOrderChange = maker.int("wait_after_order_change", 1000) val clickWaitTime = maker.int("click_wait_time", 300) val clickDuration = maker.int("click_duration", 50) diff --git a/scripts/src/main/java/io/github/fate_grand_automata/scripts/Images.kt b/scripts/src/main/java/io/github/fate_grand_automata/scripts/Images.kt index e06078b59..ec4e43ad8 100644 --- a/scripts/src/main/java/io/github/fate_grand_automata/scripts/Images.kt +++ b/scripts/src/main/java/io/github/fate_grand_automata/scripts/Images.kt @@ -8,6 +8,9 @@ enum class Images(val path: String) { Buster("buster.png"), Arts("art.png"), Quick("quick.png"), + NpBuster("np_buster.png"), + NpArts("np_art.png"), + NpQuick("np_quick.png"), Weak("weak.png"), Resist("resist.png"), Friend("friend.png"), diff --git a/scripts/src/main/java/io/github/fate_grand_automata/scripts/enums/AttackPriorityEnum.kt b/scripts/src/main/java/io/github/fate_grand_automata/scripts/enums/AttackPriorityEnum.kt new file mode 100644 index 000000000..23e05a681 --- /dev/null +++ b/scripts/src/main/java/io/github/fate_grand_automata/scripts/enums/AttackPriorityEnum.kt @@ -0,0 +1,15 @@ +package io.github.fate_grand_automata.scripts.enums + +enum class AttackPriorityEnum { + BraveChainPriority, + CardChainPriority, + ServantPriority, + CardColorPriority; + + companion object { + // It is defined separately here instead of using the order of the enum definition + // because some parts might be removed or changed in future when the actual AttackPriority implementation is done + // and this will prevent the app from breaking when serializing or deserializing content from this enum. + val defaultOrder = listOf(BraveChainPriority, CardChainPriority, ServantPriority, CardColorPriority) + } +} \ No newline at end of file diff --git a/scripts/src/main/java/io/github/fate_grand_automata/scripts/enums/BraveChainEnum.kt b/scripts/src/main/java/io/github/fate_grand_automata/scripts/enums/BraveChainEnum.kt index 4b378bb3a..7f6ed749e 100644 --- a/scripts/src/main/java/io/github/fate_grand_automata/scripts/enums/BraveChainEnum.kt +++ b/scripts/src/main/java/io/github/fate_grand_automata/scripts/enums/BraveChainEnum.kt @@ -3,5 +3,6 @@ package io.github.fate_grand_automata.scripts.enums enum class BraveChainEnum { None, WithNP, + Always, Avoid } \ No newline at end of file diff --git a/scripts/src/main/java/io/github/fate_grand_automata/scripts/enums/ChainTypeEnum.kt b/scripts/src/main/java/io/github/fate_grand_automata/scripts/enums/ChainTypeEnum.kt new file mode 100644 index 000000000..5997a5f64 --- /dev/null +++ b/scripts/src/main/java/io/github/fate_grand_automata/scripts/enums/ChainTypeEnum.kt @@ -0,0 +1,16 @@ +package io.github.fate_grand_automata.scripts.enums + +enum class ChainTypeEnum { + Mighty, + Buster, + Arts, + Quick, + None, + Avoid; + + companion object { + val Cutoff = None + val defaultOrder = listOf(Mighty, Buster, Arts, Quick, None, Avoid) + val allPermitted = listOf(Mighty, Buster, Arts, Quick, Avoid) + } +} \ No newline at end of file diff --git a/scripts/src/main/java/io/github/fate_grand_automata/scripts/locations/AttackScreenLocations.kt b/scripts/src/main/java/io/github/fate_grand_automata/scripts/locations/AttackScreenLocations.kt index fbd51528b..3f3d29616 100644 --- a/scripts/src/main/java/io/github/fate_grand_automata/scripts/locations/AttackScreenLocations.kt +++ b/scripts/src/main/java/io/github/fate_grand_automata/scripts/locations/AttackScreenLocations.kt @@ -53,6 +53,12 @@ class AttackScreenLocations @Inject constructor( CommandCard.Face.E -> 880 }.let { x -> Region(x - 100, 700, 500, 400) + faceCardDeltaY }.xFromCenter() + fun servantNPMatchRegion(card: CommandCard.NP) = when (card) { + CommandCard.NP.A -> -620 + CommandCard.NP.B -> -150 + CommandCard.NP.C -> 324 + }.let { x -> Region(x - 50, 100, 400, 400) + faceCardDeltaY }.xFromCenter() + fun supportCheckRegion(card: CommandCard.Face) = affinityRegion(card) + Location(-50, 100) diff --git a/scripts/src/main/java/io/github/fate_grand_automata/scripts/locations/BattleScreenLocations.kt b/scripts/src/main/java/io/github/fate_grand_automata/scripts/locations/BattleScreenLocations.kt index 11fef979f..a48c62ef6 100644 --- a/scripts/src/main/java/io/github/fate_grand_automata/scripts/locations/BattleScreenLocations.kt +++ b/scripts/src/main/java/io/github/fate_grand_automata/scripts/locations/BattleScreenLocations.kt @@ -120,5 +120,10 @@ class BattleScreenLocations @Inject constructor( val servantDetailsFaceCardRegion = Region(-685, 410, 110, 60).xFromCenter() + val servantNpCardTypeRegion = when (gameServer) { + is GameServer.Jp, GameServer.Cn -> 410 + else -> 330 + }.let { y -> Region(-735, y + 350, 100, 50) }.xFromCenter() + val battleSafeMiddleOfScreenClick = Location(0, 550).xFromCenter() } diff --git a/scripts/src/main/java/io/github/fate_grand_automata/scripts/models/battle/ChainPriorityPerWave.kt b/scripts/src/main/java/io/github/fate_grand_automata/scripts/models/battle/ChainPriorityPerWave.kt new file mode 100644 index 000000000..554406f46 --- /dev/null +++ b/scripts/src/main/java/io/github/fate_grand_automata/scripts/models/battle/ChainPriorityPerWave.kt @@ -0,0 +1,45 @@ +package io.github.fate_grand_automata.scripts.models.battle + +import io.github.fate_grand_automata.scripts.enums.ChainTypeEnum + +class ChainPriorityPerWave private constructor( + private val chainsPerWave: List> +) : List> by chainsPerWave { + fun atWave(wave: Int) = + chainsPerWave[wave.coerceIn(chainsPerWave.indices)] + + override fun toString() = + chainsPerWave + .joinToString(STAGE_SEPARATOR) { + it.joinToString(SEPARATOR) { c -> c.name } + } + + companion object { + val default = from(listOf(ChainTypeEnum.defaultOrder)) + private const val STAGE_SEPARATOR = "\n" + private const val SEPARATOR = "," + + fun from(chainsPerWave: List>) = + ChainPriorityPerWave(chainsPerWave) + + fun of(priority: String): ChainPriorityPerWave = + if (priority.isBlank()) { + default + } else { + ChainPriorityPerWave( + priority + .split(STAGE_SEPARATOR) + .map { priorityOnAWave -> + priorityOnAWave + .split(SEPARATOR) + .map { + ChainTypeEnum.valueOf(it) + } + .toSet() + .plusElement(ChainTypeEnum.None) + .toList() + } + ) + } + } +} \ No newline at end of file diff --git a/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/ApplyBraveChains.kt b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/ApplyBraveChains.kt index 963416306..c433d5379 100644 --- a/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/ApplyBraveChains.kt +++ b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/ApplyBraveChains.kt @@ -4,20 +4,30 @@ import io.github.fate_grand_automata.scripts.enums.BraveChainEnum import io.github.fate_grand_automata.scripts.models.NPUsage import io.github.fate_grand_automata.scripts.models.ParsedCard import io.github.fate_grand_automata.scripts.models.toFieldSlot -import io.github.lib_automata.dagger.ScriptScope import java.util.Collections -import javax.inject.Inject -@ScriptScope -class ApplyBraveChains @Inject constructor() { +object ApplyBraveChains { private fun rearrange( cards: List, rearrange: Boolean, npUsage: NPUsage ): List { - if (rearrange + // Get this out of the way asap + if (!rearrange) return cards + + /* + If there is 1 NP and 1 Card before NP, + we want the best or matching face-card after NP immediately + */ + if (listOf(npUsage.nps.size, npUsage.cardsBeforeNP).all { it == 1 }) { + return cards.toMutableList().also { + Collections.swap(it, 0, 1) + } + } + + if ( // If there are cards before NP, at max there's only 1 card after NP - && npUsage.cardsBeforeNP == 0 + npUsage.cardsBeforeNP == 0 // If there are more than 1 NPs, only 1 card after NPs at max && npUsage.nps.size <= 1 ) { @@ -58,15 +68,6 @@ class ApplyBraveChains @Inject constructor() { .toMutableList() val nonMatchingCards = cards - matchingCards val combinedCards = matchingCards + nonMatchingCards - /* - When rearrange is active and there is 1 NP and 1 Card before NP, - we want the best or matching face-card after NP. - */ - if (rearrange - && listOf(npUsage.nps.size, npUsage.cardsBeforeNP).all { it == 1 } - ) { - Collections.swap(combinedCards, 0, 1) - } return rearrange( cards = combinedCards, @@ -166,6 +167,7 @@ class ApplyBraveChains @Inject constructor() { npUsage = npUsage ) + BraveChainEnum.Always, BraveChainEnum.WithNP -> withNp( cards = cards, rearrange = rearrange, diff --git a/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/Card.kt b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/Card.kt index 6296f0bae..79c17a14e 100644 --- a/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/Card.kt +++ b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/Card.kt @@ -3,15 +3,20 @@ package io.github.fate_grand_automata.scripts.modules import io.github.fate_grand_automata.scripts.IFgoAutomataApi import io.github.fate_grand_automata.scripts.ScriptLog import io.github.fate_grand_automata.scripts.enums.BraveChainEnum +import io.github.fate_grand_automata.scripts.enums.CardTypeEnum import io.github.fate_grand_automata.scripts.models.CommandCard import io.github.fate_grand_automata.scripts.models.FieldSlot import io.github.fate_grand_automata.scripts.models.NPUsage import io.github.fate_grand_automata.scripts.models.ParsedCard import io.github.fate_grand_automata.scripts.models.SpamConfigPerTeamSlot +import io.github.fate_grand_automata.scripts.models.TeamSlot import io.github.fate_grand_automata.scripts.models.battle.BattleState +import io.github.fate_grand_automata.scripts.models.toFieldSlot +import io.github.fate_grand_automata.scripts.modules.attack.AttackPriorityHandler import io.github.fate_grand_automata.scripts.prefs.IBattleConfig import io.github.lib_automata.dagger.ScriptScope import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds @ScriptScope class Card @Inject constructor( @@ -22,7 +27,6 @@ class Card @Inject constructor( private val caster: Caster, private val parser: CardParser, private val priority: FaceCardPriority, - private val braveChains: ApplyBraveChains, private val battleConfig: IBattleConfig ) : IFgoAutomataApi by api { @@ -57,11 +61,27 @@ class Card @Inject constructor( val braveChainsPerWave = battleConfig.braveChains val rearrangeCardsPerWave = battleConfig.rearrangeCards - return braveChains.pick( + val useChainPriority = battleConfig.useChainPriority + if (useChainPriority) { + val npUsageToUse = NPUsage(npUsage.nps + spamNps.detected(), npUsage.cardsBeforeNP) + + val npTypes = npUsageToUse.nps.associate { it.toFieldSlot() to it.type() } + val chainPriority = battleConfig.chainPriority.atWave(state.stage) + return AttackPriorityHandler.pick( + cards = cardsOrderedByPriority, + npUsage = npUsageToUse, + braveChainEnum = braveChainsPerWave.inCurrentWave(BraveChainEnum.None), + chainPriority = chainPriority, + rearrange = rearrangeCardsPerWave.inCurrentWave(false), + npTypes = npTypes, + ).map { it.card } + } + + return ApplyBraveChains.pick( cards = cardsOrderedByPriority, npUsage = npUsage, braveChains = braveChainsPerWave.inCurrentWave(BraveChainEnum.None), - rearrange = rearrangeCardsPerWave.inCurrentWave(false) + rearrange = rearrangeCardsPerWave.inCurrentWave(false), ).map { it.card } } @@ -92,4 +112,23 @@ class Card @Inject constructor( .also { messages.log(ScriptLog.ClickingCards(it)) } .forEach { caster.use(it) } } + + fun CommandCard.NP.type(): CardTypeEnum { + val fieldSlot = this.toFieldSlot() + val teamSlot = servantTracker.deployed[fieldSlot] ?: return CardTypeEnum.Unknown + if (teamSlot is TeamSlot.Unknown) return CardTypeEnum.Unknown + return servantTracker.getNpCardType(teamSlot) + } + + fun NPUsage.detected (): NPUsage { + val validNPs = (this.nps + spamNps).detected() + return NPUsage(validNPs, this.cardsBeforeNP) + } + + fun Set.detected (): Set { + val npCardsDetected = servantTracker.npCardsDetectedUsingServantFaces() + return this + .filter { it in npCardsDetected } + .toSet() + } } \ No newline at end of file diff --git a/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/Caster.kt b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/Caster.kt index a5544f3be..69562661e 100644 --- a/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/Caster.kt +++ b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/Caster.kt @@ -164,7 +164,7 @@ class Caster @Inject constructor( waitForAnimationToFinish(15.seconds) // Extra wait for the lag introduced by Order change - 1.seconds.wait() + prefs.waitAfterOrderChange.wait() servantTracker.orderChanged(action.starting, action.sub) } diff --git a/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/ServantTracker.kt b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/ServantTracker.kt index 5e9d48ff3..53610b65c 100644 --- a/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/ServantTracker.kt +++ b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/ServantTracker.kt @@ -3,14 +3,19 @@ package io.github.fate_grand_automata.scripts.modules import io.github.fate_grand_automata.scripts.IFgoAutomataApi import io.github.fate_grand_automata.scripts.Images import io.github.fate_grand_automata.scripts.ScriptLog +import io.github.fate_grand_automata.scripts.enums.CardTypeEnum import io.github.fate_grand_automata.scripts.models.CommandCard import io.github.fate_grand_automata.scripts.models.FieldSlot import io.github.fate_grand_automata.scripts.models.OrderChangeMember import io.github.fate_grand_automata.scripts.models.TeamSlot import io.github.fate_grand_automata.scripts.models.skills +import io.github.fate_grand_automata.scripts.models.toFieldSlot import io.github.lib_automata.Pattern import io.github.lib_automata.dagger.ScriptScope import javax.inject.Inject +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.mapValues import kotlin.time.Duration.Companion.milliseconds @ScriptScope @@ -57,12 +62,30 @@ class ServantTracker @Inject constructor( private val faceCardImages = mutableMapOf>() + /** + * NP card type handling + */ + private val npCardImages = mutableMapOf>() + private val npSplashImages = mapOf( + CardTypeEnum.Buster to images[Images.NpBuster], + CardTypeEnum.Arts to images[Images.NpArts], + CardTypeEnum.Quick to images[Images.NpQuick], + ) + val npCardTypes = mutableMapOf() + + fun getNpCardType(teamSlot: TeamSlot): CardTypeEnum { + return npCardTypes.getOrElse(teamSlot) { CardTypeEnum.Unknown } + } + + override fun close() { checkImages.values.forEach { it.close() } faceCardImages.values.flatten().forEach { it.close() } + npCardImages.values.flatten().forEach { it.close() } checkImages.clear() } + private fun init(teamSlot: TeamSlot, slot: FieldSlot) { messages.log( ScriptLog.ServantEnteredSlot( @@ -92,9 +115,30 @@ class ServantTracker @Inject constructor( if (supportSlot == null && isSupport) { supportSlot = teamSlot - } else if (!isSupport) { - // Don't useSameSnapIn here, since we open a dialog - initFaceCard(teamSlot, slot) + } + + // We now always want to init the face card, so that we can check the npType + // Don't useSameSnapIn here, since we open a dialog + initFaceCard(teamSlot, slot) + + // After initFaceCard, we need to check npType for servant here + if (npCardImages.contains(teamSlot)) { + val patternList = npCardImages[teamSlot] + val scores = npSplashImages + .mapValues { + (_, image) -> patternList?.maxOf { + pattern -> pattern.find(image, prefs.npCardTypeSimilarity)?.score ?: 0.0 + } ?: 0.0 + } + // Get type by comparing the patterns we have with the splash arts + val type = scores + .filterValues { it > 0.0 } + .maxByOrNull { it.value } + ?.key + + if (type != null) { + npCardTypes.put(teamSlot, type) + } } } @@ -109,12 +153,15 @@ class ServantTracker @Inject constructor( 250.milliseconds.wait() val image = locations.battle.servantDetailsFaceCardRegion.getPattern("Face $teamSlot") + val npImage = locations.battle.servantNpCardTypeRegion.getPattern("NP type $teamSlot") // Close dialog locations.battle.extraInfoWindowCloseClick.click() faceCardImages.getOrPut(teamSlot) { mutableListOf() } .add(image) + npCardImages.getOrPut(teamSlot) { mutableListOf() } + .add(npImage) 250.milliseconds.wait() } @@ -208,8 +255,12 @@ class ServantTracker @Inject constructor( } } + // Fallback for if the support servant is not found + val currentSupportSlot = supportSlot + val isSupportFound = if (currentSupportSlot != null) (result.getOrElse(currentSupportSlot) { emptySet() }).isNotEmpty() else false + val ownedServants = faceCardImages - .filterKeys { it != supportSlot && it in deployed.values } + .filterKeys { (isSupportFound || it != supportSlot) && it in deployed.values } cardsRemaining .groupBy { card -> // find the best matching Servant which isn't the support @@ -241,6 +292,37 @@ class ServantTracker @Inject constructor( return result } + fun npCardsDetectedUsingServantFaces(): Set { + if (prefs.skipServantFaceCardCheck) { + return emptySet() + } + + val npCards = CommandCard.NP.list.toSet() + val deployedServants = faceCardImages + .filterKeys { it in deployed.values } + + val scores = npCards + .map { npCard -> + npCard to deployedServants + .filterKeys { it.toFieldSlot() == npCard.toFieldSlot() } + .mapValues { (_, images) -> + images.maxOf { image -> + locations.attack.servantNPMatchRegion(npCard) + .find(image, 0.4)?.score ?: 0.0 + } + } + .values + } + val result = scores + .filter { (_, values) -> + values.filter { it > prefs.npSpamCardDetectionSimilarity }.maxByOrNull { it } != null + } + .map { it.first } + .toSet() + + return result + } + /** * Adds the 3rd Ascension Melusine image to the existing 1st/2nd Ascension * image so both are detected as the same Servant. @@ -263,4 +345,11 @@ class ServantTracker @Inject constructor( */ private fun isSupport(slot: FieldSlot) = !prefs.treatSupportLikeOwnServant && images[Images.ServantCheckSupport] in locations.battle.servantChangeSupportCheckRegion(slot) + + fun TeamSlot.toFieldSlot(): FieldSlot? { + return deployed + .entries + .firstOrNull { (_, teamSlot) -> teamSlot == this } + ?.key + } } diff --git a/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/attack/AttackPriorityHandler.kt b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/attack/AttackPriorityHandler.kt new file mode 100644 index 000000000..fe72d3f92 --- /dev/null +++ b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/attack/AttackPriorityHandler.kt @@ -0,0 +1,116 @@ +package io.github.fate_grand_automata.scripts.modules.attack + +import io.github.fate_grand_automata.scripts.enums.AttackPriorityEnum +import io.github.fate_grand_automata.scripts.enums.BraveChainEnum +import io.github.fate_grand_automata.scripts.enums.CardTypeEnum +import io.github.fate_grand_automata.scripts.enums.ChainTypeEnum +import io.github.fate_grand_automata.scripts.models.FieldSlot +import io.github.fate_grand_automata.scripts.models.NPUsage +import io.github.fate_grand_automata.scripts.models.ParsedCard +import java.util.Collections + +object AttackPriorityHandler { + fun rearrange( + cards: List, + rearrange: Boolean, + npUsage: NPUsage + ): List { + /* + When rearrange is active and there is 1 NP and 1 Card before NP, + we want the best or matching face-card after NP. + */ + val shouldSwapForNpUsageScenario = listOf(npUsage.nps.size, npUsage.cardsBeforeNP).all { it == 1 } + if (rearrange && shouldSwapForNpUsageScenario) { + return cards.toMutableList().also { + Collections.swap(it, 0, 1) + } + } + + if (rearrange + // If there are cards before NP, at max there's only 1 card after NP + && npUsage.cardsBeforeNP == 0 + // If there are more than 1 NPs, only 1 card after NPs at max + && npUsage.nps.size <= 1 + ) { + val cardsToRearrange = List(cards.size) { index -> index } + .take((3 - npUsage.nps.size).coerceAtLeast(0)) + .reversed() + + // When clicking 3 cards, move the card with 2nd highest priority to last position to amplify its effect + // Do the same when clicking 2 cards unless they're used before NPs. + if (cardsToRearrange.size in 2..3) { + return cards.toMutableList().also { + Collections.swap(it, cardsToRearrange[1], cardsToRearrange[0]) + } + } + } + + return cards + } + + fun pick( + cards: List, + attackPriorityOrder: List = AttackPriorityEnum.defaultOrder, + chainPriority: List = ChainTypeEnum.defaultOrder, + braveChainEnum: BraveChainEnum = BraveChainEnum.None, + npUsage: NPUsage = NPUsage.none, + npTypes: Map = emptyMap(), + rearrange: Boolean = false, + ): List { + // All stunned cards are categorized as Unknown. + // Filter all of them since those cards are to be avoided if possible + // and the system does not deal with CardTypeEnum.Unknown + val nonUnknownCards = cards.filter { it.type != CardTypeEnum.Unknown } + val finalFallback = nonUnknownCards + (cards - nonUnknownCards) + if (!AttackUtils.isChainable( + cards = nonUnknownCards, + npUsage = npUsage, + npTypes = npTypes, + )) { + return finalFallback + } + + // Get all the supplementary data + val cardCountPerFieldSlotMap = AttackUtils.getCardsPerFieldSlotMap(cards, npUsage) + val cardCountPerCardTypeMap = AttackUtils.getCardsPerCardTypeMap(cards, npTypes) + + // Start + var newCardOrder: List? = null + var braveChainFallback: List? = null + for (attackPriority in attackPriorityOrder) { + when (attackPriority) { + AttackPriorityEnum.BraveChainPriority -> { + braveChainFallback = BraveChainHandler.pick( + cards = nonUnknownCards, + braveChainEnum = braveChainEnum, + npUsage = npUsage, + cardCountPerFieldSlotMap = cardCountPerFieldSlotMap, + ) + } + AttackPriorityEnum.CardChainPriority -> { + if (newCardOrder != null) continue + + newCardOrder = CardChainPriorityHandler.pick( + cards = nonUnknownCards, + chainPriority = chainPriority, + braveChainEnum = braveChainEnum, + npUsage = npUsage, + npTypes = npTypes, + cardCountPerFieldSlotMap = cardCountPerFieldSlotMap, + cardCountPerCardTypeMap = cardCountPerCardTypeMap, + // BraveChain is higher priority than color chain + forceBraveChain = braveChainFallback != null + && (braveChainEnum == BraveChainEnum.WithNP || braveChainEnum == BraveChainEnum.Always) + ) + } + else -> continue + } + } + + return rearrange( + cards = newCardOrder ?: braveChainFallback ?: finalFallback, + rearrange = rearrange, + npUsage = npUsage + ) + } +} \ No newline at end of file diff --git a/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/attack/AttackUtils.kt b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/attack/AttackUtils.kt new file mode 100644 index 000000000..8bca45087 --- /dev/null +++ b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/attack/AttackUtils.kt @@ -0,0 +1,127 @@ +package io.github.fate_grand_automata.scripts.modules.attack + +import io.github.fate_grand_automata.scripts.enums.BraveChainEnum +import io.github.fate_grand_automata.scripts.enums.CardTypeEnum +import io.github.fate_grand_automata.scripts.models.FieldSlot +import io.github.fate_grand_automata.scripts.models.NPUsage +import io.github.fate_grand_automata.scripts.models.ParsedCard +import io.github.fate_grand_automata.scripts.models.toFieldSlot + +object AttackUtils { + fun getCardsPerFieldSlotMap ( + cards: List, + npUsage: NPUsage = NPUsage.none + ): Map { + // Card list check + val cardsPerFieldSlotMap: MutableMap = mutableMapOf() + for (card in cards) { + val fieldSlot = card.fieldSlot + if (fieldSlot == null) continue + val currentValue = cardsPerFieldSlotMap.getOrElse(fieldSlot) { 0 } + cardsPerFieldSlotMap[fieldSlot] = currentValue + 1 + } + // NPs check + // Any more than 1 NP means that it is impossible to Brave Chain + // 0 NPs does not need handling + if (npUsage.nps.size == 1) { + val fieldSlot = npUsage.nps.first().toFieldSlot() + val currentValue = cardsPerFieldSlotMap.getOrElse(fieldSlot) { 0 } + cardsPerFieldSlotMap[fieldSlot] = currentValue + 1 + } + return cardsPerFieldSlotMap.toMap() + } + fun getCardsPerCardTypeMap ( + cards: List, + npTypes: Map = emptyMap(), + ): Map { + // Card list check + val cardsPerCardTypeMap: MutableMap = mutableMapOf() + for (card in cards) { + val cardType = card.type + val currentValue = cardsPerCardTypeMap.getOrElse(cardType) { 0 } + cardsPerCardTypeMap[cardType] = currentValue + 1 + } + // NPs check + for (npType in npTypes.values) { + val currentValue = cardsPerCardTypeMap.getOrElse(npType) { 0 } + cardsPerCardTypeMap[npType] = currentValue + 1 + } + return cardsPerCardTypeMap.toMap() + } + + fun getFieldSlotsWithValidBraveChain ( + cards: List, + npUsage: NPUsage = NPUsage.none + ): List { + val cardsPerFieldSlotMap = getCardsPerFieldSlotMap(cards, npUsage) + return getFieldSlotsWithValidBraveChain(cardsPerFieldSlotMap) + } + + fun getFieldSlotsWithValidBraveChain ( + cardsPerFieldSlotMap: Map + ): List { + return cardsPerFieldSlotMap.keys + .filter { cardsPerFieldSlotMap.getOrElse(it) { 0 } >= 3 } + .map { it } + } + + /** + * Returns a field slot for a BraveChain if there is one available + * @param braveChainEnum BraveChainEnum that determines the behavior of this function. + * BraveChainEnum.None -> Does not return a fieldSlot, since BraveChains are not forced. + * BraveChainEnum.Avoid -> Does not return a fieldSlot, since BraveChains are avoided. + * BraveChainEnum.WithNP -> Returns a field slot if there is 1 NP with a valid BraveChain. + * BraveChainEnum.Always -> Always returns a fieldSlot if there is a valid one. + * @param cards The list of cards available to choose from + * @param npUsage NPs that have been clicked + * @returns A valid FieldSlot for a BraveChain or null if there is none + */ + fun getBraveChainFieldSlot ( + braveChainEnum: BraveChainEnum = BraveChainEnum.None, + cards: List, + npUsage: NPUsage = NPUsage.none, + ): FieldSlot? { + if ( + braveChainEnum == BraveChainEnum.Avoid + || braveChainEnum == BraveChainEnum.None + // Since it is impossible to make a BraveChain + || npUsage.nps.size > 1) return null + else if (npUsage.nps.size == 1) { + // Get np if there is only 1 (since we want to try for Brave Chain) + val firstNp = npUsage.nps.firstOrNull() + val fieldSlot = firstNp?.toFieldSlot() + // Only return the field slot if it is valid for a Brave Chain + if (cards.filter { it.fieldSlot == fieldSlot }.size >= 2) return fieldSlot + } else if (braveChainEnum == BraveChainEnum.Always) { + // Force brave chain only if it always wants a Brave Chain + val braveChainCapableFieldSlots = getFieldSlotsWithValidBraveChain(cards, npUsage) + val braveChainPriorityCard = cards.firstOrNull { braveChainCapableFieldSlots.contains(it.fieldSlot) } + // Return the first valid one, since it is already the highest priority + return braveChainPriorityCard?.fieldSlot + } + return null + } + + /** + * @returns A list of non-Unknown type cards that can be used to make a Chain. + */ + fun getValidNonUnknownCards ( + cards: List + ): List { + // Try to ensure unknown is handled + return cards.filter { it.type != CardTypeEnum.Unknown } + } + + /** + * @returns Are there enough cards to make a chain? + */ + fun isChainable ( + cards: List, + npUsage: NPUsage = NPUsage.none, + npTypes: Map = emptyMap(), + ): Boolean { + if (npTypes.values.contains(CardTypeEnum.Unknown)) return false + val cardsNeeded = 3 - npUsage.nps.size + return getValidNonUnknownCards(cards).size >= cardsNeeded + } +} \ No newline at end of file diff --git a/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/attack/AvoidChainHandler.kt b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/attack/AvoidChainHandler.kt new file mode 100644 index 000000000..30645d845 --- /dev/null +++ b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/attack/AvoidChainHandler.kt @@ -0,0 +1,147 @@ +package io.github.fate_grand_automata.scripts.modules.attack + +import io.github.fate_grand_automata.scripts.enums.BraveChainEnum +import io.github.fate_grand_automata.scripts.enums.CardTypeEnum +import io.github.fate_grand_automata.scripts.models.FieldSlot +import io.github.fate_grand_automata.scripts.models.NPUsage +import io.github.fate_grand_automata.scripts.models.ParsedCard +import io.github.fate_grand_automata.scripts.models.toFieldSlot +import kotlin.collections.set + +object AvoidChainHandler { + fun pick( + cards: List, + braveChainEnum: BraveChainEnum = BraveChainEnum.Avoid, + npUsage: NPUsage = NPUsage.none, + npTypes: Map = emptyMap(), + avoidCardChains: Boolean = true, + cardCountPerFieldSlotMap: Map? = null, + cardCountPerCardTypeMap: Map? = null, + ): List? { + // If there are already 3 NPs, there is nothing to avoid + if (npUsage.nps.size == 3) return null + + // Try to ensure unknown is handled + val nonUnknownCards = AttackUtils.getValidNonUnknownCards(cards) + if (!AttackUtils.isChainable( + cards = nonUnknownCards, + npUsage = npUsage, + npTypes = npTypes, + )) return null + + if (npUsage.nps.size == 2) { + // No matter what is returned, it will be valid for sure + if (!avoidCardChains) return nonUnknownCards + (cards - nonUnknownCards) + // Otherwise, need to check if they have the same NP type + } + + // Handle brave chains + // If there is a valid braveChainFieldSlot, it means that a Brave Chain must be made while avoiding + // Color + Mighty Chains while avoidCardChains = true + val avoidBraveChains = braveChainEnum == BraveChainEnum.Avoid + val braveChainFieldSlot = AttackUtils.getBraveChainFieldSlot( + braveChainEnum = braveChainEnum, + cards = nonUnknownCards, + npUsage = npUsage, + ) + + // This is just a normal BraveChain requirement. Go to BraveChainHandler for this + if (braveChainFieldSlot != null && !avoidCardChains) return null + + // Filter BraveChain-able only cards or skip filter (if there is no BraveChain required) + val cardsToUse = nonUnknownCards.filter { braveChainFieldSlot == null || it.fieldSlot == braveChainFieldSlot } + + val cardCountPerFieldSlotMap = AttackUtils.getCardsPerFieldSlotMap(cardsToUse, npUsage) + val cardCountPerCardTypeMap = AttackUtils.getCardsPerCardTypeMap(cardsToUse, npTypes) + + // If only a BraveChain is possible and it is avoidBraveChains + if (avoidBraveChains && cardCountPerFieldSlotMap.size == 1) return null + + // If only a ColorChain is possible + if (avoidCardChains && cardCountPerCardTypeMap.size == 1) return null + + // For tracking + val selectedFieldSlots = mutableMapOf() + val selectedCardTypes = mutableMapOf() + + if (avoidBraveChains) { + for (np in npUsage.nps) { + val fieldSlot = np.toFieldSlot() + val currentValue = selectedFieldSlots.getOrElse(fieldSlot) { 0 } + selectedFieldSlots[fieldSlot] = currentValue + 1 + } + } + if (avoidCardChains) { + for (npType in npTypes.values) { + val currentValue = selectedCardTypes.getOrElse(npType) { 0 } + selectedCardTypes[npType] = currentValue + 1 + } + } + + var cachedFilteredCards: List = cardsToUse + var previousFieldSlot: FieldSlot? = null + + // Otherwise, calculate + val cardsNeeded = 3 - npUsage.nps.size + val selectedCards = mutableListOf() + while (selectedCards.size < cardsNeeded) { + // Loop through all the cards at least one time so that the next pass is easier + var filteredCards = mutableListOf() + for (card in cachedFilteredCards) { + if (card in selectedCards) continue + val fieldSlot = card.fieldSlot + if (fieldSlot == null) continue + val fieldSlotCount = selectedFieldSlots.getOrElse(fieldSlot) { 0 } + if (avoidBraveChains + && ( + // If there are already 2 selected cards with the same card slot + fieldSlotCount >= 2 + // and there are still valid targets, skip + && cardCountPerFieldSlotMap.size > 1 + ) + ) continue + + val cardType = card.type + val cardTypeCount = selectedCardTypes.getOrElse(cardType) { 0 } + if (avoidCardChains + && ( + cardTypeCount >= 2 + // Or if it would make a Mighty Chain + || (cardTypeCount == 0 && selectedCardTypes.size >= 2) + ) + ) continue + // If reach here, add to filteredList + filteredCards.add(card) + } + + // Cache the filteredCards to make the next check faster + cachedFilteredCards = filteredCards + + // Try to make the next card different from the previous card's fieldSlot, if possible + // and there must not be a valid BraveChain for WithNP and Always + if (previousFieldSlot != null && braveChainFieldSlot == null) { + val fieldSlotFilter = filteredCards.filter { + it.fieldSlot != previousFieldSlot + } + if (fieldSlotFilter.isNotEmpty()) filteredCards = fieldSlotFilter.toMutableList() + } + + val filteredCard = filteredCards.firstOrNull() + if (filteredCard == null || filteredCard.fieldSlot == null) break // If cannot find, leave + + // If reach here, card can be added + selectedCards.add(filteredCard) + selectedFieldSlots[filteredCard.fieldSlot] = selectedFieldSlots.getOrElse(filteredCard.fieldSlot) { 0 } + 1 + selectedCardTypes[filteredCard.type] = selectedCardTypes.getOrElse(filteredCard.type) { 0 } + 1 + previousFieldSlot = filteredCard.fieldSlot + } + + // Return empty if cannot fulfill + if (selectedCards.size < cardsNeeded) return null + + val remainder = (cardsToUse - selectedCards) + (cards - cardsToUse) + val combinedCards = selectedCards + remainder + + return combinedCards + } +} \ No newline at end of file diff --git a/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/attack/BraveChainHandler.kt b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/attack/BraveChainHandler.kt new file mode 100644 index 000000000..d7e707341 --- /dev/null +++ b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/attack/BraveChainHandler.kt @@ -0,0 +1,76 @@ +package io.github.fate_grand_automata.scripts.modules.attack + +import io.github.fate_grand_automata.scripts.enums.BraveChainEnum +import io.github.fate_grand_automata.scripts.models.FieldSlot +import io.github.fate_grand_automata.scripts.models.NPUsage +import io.github.fate_grand_automata.scripts.models.ParsedCard +import io.github.fate_grand_automata.scripts.models.toFieldSlot + +object BraveChainHandler { + fun pick( + npUsage: NPUsage = NPUsage.none, + cards: List, + braveChainEnum: BraveChainEnum, + cardCountPerFieldSlotMap: Map? = null, + ): List? { + if (braveChainEnum == BraveChainEnum.Avoid) { + return AvoidChainHandler.pick( + cards = cards, + npUsage = npUsage, + braveChainEnum = braveChainEnum, + avoidCardChains = false, + ) + } + + // Try to ensure unknown is handled + val nonUnknownCards = AttackUtils.getValidNonUnknownCards(cards) + if (!AttackUtils.isChainable( + cards = nonUnknownCards, + npUsage = npUsage, + )) { + return null + } + + val cardsPerFieldSlotMap = cardCountPerFieldSlotMap ?: AttackUtils.getCardsPerFieldSlotMap(nonUnknownCards, npUsage) + if (!isBraveChainAllowed(braveChainEnum, npUsage, cardsPerFieldSlotMap)) return null + + // Always returns a valid field slot if there is one for BraveChain + val priorityFieldSlot = AttackUtils.getBraveChainFieldSlot( + braveChainEnum = braveChainEnum, + cards = nonUnknownCards, + npUsage = npUsage, + ) + if (priorityFieldSlot == null) return null + + val selectedCards = nonUnknownCards + .filter { it.fieldSlot == priorityFieldSlot } + .take((3 - npUsage.nps.size).coerceAtLeast(0)) + val remainder = (nonUnknownCards - selectedCards) + (cards - nonUnknownCards) + return selectedCards + remainder + } + + private fun isBraveChainAllowed ( + braveChainEnum: BraveChainEnum, + npUsage: NPUsage = NPUsage.none, + cardsPerFieldSlotMap: Map + ): Boolean { + if ( + braveChainEnum == BraveChainEnum.Avoid + || cardsPerFieldSlotMap.values.isEmpty() + ) return false + + if (npUsage.nps.size == 1) { + // Permit brave chain with NP only + val npFieldSlot = npUsage.nps.firstOrNull()?.toFieldSlot() + if (npFieldSlot != null) { + return cardsPerFieldSlotMap.getOrElse(npFieldSlot) { 0 } >= 3 + } + } + + // Sort by most number of cards + val highestTotalCardCount = cardsPerFieldSlotMap.values.sorted().reversed().first() + + return highestTotalCardCount >= 3 + } + +} \ No newline at end of file diff --git a/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/attack/CardChainPriorityHandler.kt b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/attack/CardChainPriorityHandler.kt new file mode 100644 index 000000000..623adfee2 --- /dev/null +++ b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/attack/CardChainPriorityHandler.kt @@ -0,0 +1,81 @@ +package io.github.fate_grand_automata.scripts.modules.attack + +import io.github.fate_grand_automata.scripts.enums.BraveChainEnum +import io.github.fate_grand_automata.scripts.enums.CardTypeEnum +import io.github.fate_grand_automata.scripts.enums.ChainTypeEnum +import io.github.fate_grand_automata.scripts.models.FieldSlot +import io.github.fate_grand_automata.scripts.models.NPUsage +import io.github.fate_grand_automata.scripts.models.ParsedCard +import kotlin.collections.plus + +object CardChainPriorityHandler { + fun pick( + cards: List, + chainPriority: List = ChainTypeEnum.defaultOrder, + braveChainEnum: BraveChainEnum = BraveChainEnum.None, + npUsage: NPUsage = NPUsage.none, + npTypes: Map = emptyMap(), + cardCountPerFieldSlotMap: Map? = null, + cardCountPerCardTypeMap: Map? = null, + forceBraveChain: Boolean = false, + ): List? { + // Try to ensure unknown is handled + val nonUnknownCards = AttackUtils.getValidNonUnknownCards(cards) + if (!AttackUtils.isChainable( + cards = nonUnknownCards, + npUsage = npUsage, + npTypes = npTypes, + )) { + return nonUnknownCards + (cards - nonUnknownCards) + } + + var newCardOrder: List? = null + val cardCountPerFieldSlotMap = cardCountPerFieldSlotMap ?: AttackUtils.getCardsPerFieldSlotMap(nonUnknownCards, npUsage) + val cardCountPerCardTypeMap = cardCountPerCardTypeMap ?: AttackUtils.getCardsPerCardTypeMap(nonUnknownCards, npTypes) + + // If forceBraveChain, set the braveChainEnum directly here + val braveChainEnum = if (forceBraveChain) BraveChainEnum.Always else braveChainEnum + + for (chain in chainPriority) { + if (newCardOrder != null) continue + newCardOrder = when (chain) { + ChainTypeEnum.None -> break; + ChainTypeEnum.Mighty -> MightyChainHandler.pick( + cards = nonUnknownCards, + npUsage = npUsage, + npTypes = npTypes, + braveChainEnum = braveChainEnum, + cardCountPerCardTypeMap = cardCountPerCardTypeMap, + cardCountPerFieldSlotMap = cardCountPerFieldSlotMap, + forceBraveChain = forceBraveChain, + ) + ChainTypeEnum.Arts, + ChainTypeEnum.Buster, + ChainTypeEnum.Quick -> ColorChainHandler.pick( + cardType = when (chain) { + ChainTypeEnum.Buster -> CardTypeEnum.Buster + ChainTypeEnum.Arts -> CardTypeEnum.Arts + ChainTypeEnum.Quick -> CardTypeEnum.Quick + else -> CardTypeEnum.Unknown + }, + cards = nonUnknownCards, + npUsage = npUsage, + npTypes = npTypes, + braveChainEnum = braveChainEnum, + cardCountPerCardTypeMap = cardCountPerCardTypeMap, + cardCountPerFieldSlotMap = cardCountPerFieldSlotMap, + forceBraveChain = forceBraveChain, + ) + ChainTypeEnum.Avoid -> AvoidChainHandler.pick( + cards = nonUnknownCards, + npUsage = npUsage, + npTypes = npTypes, + braveChainEnum = braveChainEnum, + avoidCardChains = true, + ) + } + } + + return newCardOrder + } +} \ No newline at end of file diff --git a/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/attack/ColorChainHandler.kt b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/attack/ColorChainHandler.kt new file mode 100644 index 000000000..a12245646 --- /dev/null +++ b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/attack/ColorChainHandler.kt @@ -0,0 +1,156 @@ +package io.github.fate_grand_automata.scripts.modules.attack + +import io.github.fate_grand_automata.scripts.enums.BraveChainEnum +import io.github.fate_grand_automata.scripts.enums.CardTypeEnum +import io.github.fate_grand_automata.scripts.models.FieldSlot +import io.github.fate_grand_automata.scripts.models.NPUsage +import io.github.fate_grand_automata.scripts.models.ParsedCard +import io.github.fate_grand_automata.scripts.models.toFieldSlot +import kotlin.collections.Map + +object ColorChainHandler { + // Returns null if uniqueCards cannot be found + fun pick ( + cardType: CardTypeEnum, + cards: List, + npUsage: NPUsage = NPUsage.none, + npTypes: Map = emptyMap(), + braveChainEnum: BraveChainEnum = BraveChainEnum.None, + cardCountPerFieldSlotMap: Map? = null, + cardCountPerCardTypeMap: Map? = null, + forceBraveChain: Boolean = false, + ): List? { + // NEVER want to make a chain of Unknown cards + if (cardType == CardTypeEnum.Unknown) return null + + val cardCountPerCardTypeMap = cardCountPerCardTypeMap ?: AttackUtils.getCardsPerCardTypeMap(cards, npTypes) + if (!isColorChainAllowed(cardType, cards, npTypes, cardCountPerCardTypeMap)) return null + + // Check for Brave Chain + val braveChainFieldSlot = AttackUtils.getBraveChainFieldSlot( + braveChainEnum = braveChainEnum, + cards = cards, + npUsage = npUsage, + ) + + // if it passes the above, the actual implementation is very easy + val cardsNeeded = 3 - npUsage.nps.size + var selectedCards = cards.filter { it.type == cardType } + + // Do a pre-emptive check + if (!AttackUtils.isChainable( + cards = selectedCards, + npUsage = npUsage, + npTypes = npTypes, + )) { + return null + } + + // Do not have any BraveChains or it is invalid + if (braveChainEnum == BraveChainEnum.Avoid) { + val newSelection = getCardsForAvoidBraveChain( + cards = cards, + selectedCards = selectedCards, + npUsage = npUsage, + cardCountPerFieldSlotMap = cardCountPerFieldSlotMap, + ) + // if it is null, it means that it is impossible to get a list that avoids a BraveChain + // if it is not null, there is a valid non-Brave Chain option + if (newSelection == null) return null + selectedCards = newSelection.toMutableList() + } + // If there is a braveChainFieldSlot, try for a Brave Color Chain + else if (braveChainFieldSlot != null) { + // Attempt to find one matching the fieldSlot + val fieldSlotList = selectedCards.filter { + it.fieldSlot == braveChainFieldSlot + } + // If there is a valid number of cards for the fieldSlot, attempt to Brave Chain + // Even if it is not valid, if BraveChainEnum.Always, + // it only accepts Brave Color Chains and not normal Color Chains + if (fieldSlotList.size >= cardsNeeded || forceBraveChain) + selectedCards = fieldSlotList + } + + // Secondary check + if (selectedCards.size < cardsNeeded) return null + val combinedCards = selectedCards + (cards - selectedCards) + return combinedCards + } + + fun isColorChainAllowed ( + cardType: CardTypeEnum, + cards: List, + npTypes: Map = emptyMap(), + cardCountPerCardTypeMap: Map? = null + ): Boolean { + // NEVER want to make a chain of Unknown cards + if (cardType == CardTypeEnum.Unknown) return false + + val uniqueCardTypesFromNp = npTypes.values.toSet() + // Impossible if the unique number of card types are not 1 + if (uniqueCardTypesFromNp.size > 1) return false + // Impossible if the nps are not of the chain type + if (uniqueCardTypesFromNp.size == 1 && uniqueCardTypesFromNp.firstOrNull() != cardType) return false + + val cardCountPerCardTypeMap = cardCountPerCardTypeMap ?: AttackUtils.getCardsPerCardTypeMap(cards, npTypes) + + // Ensure there are at least 3 cards of the type + // This innately handles Unknown for Color chain + return cardCountPerCardTypeMap.getOrElse(cardType) { 0 } >= 3 + } + + /** + * Tries to get a list of valid cards for BraveChainEnum.Avoid while fulfilling the color requirement of this Chain. + * + * @param cards The list of cards available to choose from + * @param npUsage NPs that have been clicked + * @param selectedCards The list of cards that fulfil the color chain + * @param cardCountPerFieldSlotMap Optional passing of map from parent to reuse. Otherwise, this function will generate its own. + * @return null if there is no valid selectable options, otherwise a valid list of ParsedCards. + */ + fun getCardsForAvoidBraveChain ( + cards: List, + selectedCards: List, + npUsage: NPUsage = NPUsage.none, + cardCountPerFieldSlotMap: Map? = null, + ): List? { + val cardsNeeded = 3 - npUsage.nps.size + if (selectedCards.size < cardsNeeded) return null + + val cardCountPerFieldSlotMap = cardCountPerFieldSlotMap ?: AttackUtils.getCardsPerFieldSlotMap(cards, npUsage) + // If there is only 1 unique field slot throughout, this is a valid entry (since it is impossible to avoid Brave Chain) + if (cardCountPerFieldSlotMap.size == 1) return selectedCards + + val npFieldSlots = npUsage.nps.map { it.toFieldSlot() }.toSet() + // If there is 2 or more NP, it will never be a BraveChain, so any cards are valid + if (npFieldSlots.size >= 2) return selectedCards + + val selectedCardsFieldSlotSet = selectedCards.mapNotNull { it.fieldSlot }.toSet() + // If the cards available are not 1 fieldSlot, but the selected cards are 1 fieldSlot only + if (selectedCardsFieldSlotSet.size == 1) return null // invalid + + val npFieldSlot = npFieldSlots.firstOrNull() + + // Otherwise, a valid list of cards exist, but the correct cards must be selected + val filteredCards = selectedCards.take(cardsNeeded).toMutableList() + val filteredCardsSet = filteredCards.mapNotNull { it.fieldSlot }.toSet() + if ( + // If all cards are the same field slot + filteredCardsSet.size == 1 + // it is the same slot as the NP + && (npFieldSlot == filteredCardsSet.first() + // or the NP is null + || npFieldSlot == null + ) + ) { + // Attempt to fetch a different field slot + val differentCard = cards.firstOrNull { it.fieldSlot != filteredCardsSet.first() } + // If there is no different card (even though there should, by this stage), return null + if (differentCard == null) return null + // Just add new card to index 1, aka 2nd card + filteredCards.add(1, differentCard) + } + return filteredCards + (cards - filteredCards) + } +} \ No newline at end of file diff --git a/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/attack/MightyChainHandler.kt b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/attack/MightyChainHandler.kt new file mode 100644 index 000000000..9ac27a769 --- /dev/null +++ b/scripts/src/main/java/io/github/fate_grand_automata/scripts/modules/attack/MightyChainHandler.kt @@ -0,0 +1,221 @@ +package io.github.fate_grand_automata.scripts.modules.attack + +import io.github.fate_grand_automata.scripts.enums.BraveChainEnum +import io.github.fate_grand_automata.scripts.enums.CardTypeEnum +import io.github.fate_grand_automata.scripts.models.FieldSlot +import io.github.fate_grand_automata.scripts.models.NPUsage +import io.github.fate_grand_automata.scripts.models.ParsedCard +import io.github.fate_grand_automata.scripts.models.toFieldSlot +import kotlin.collections.Map + +object MightyChainHandler { + // We want 3 unique types, the magic number + const val TOTAL_UNIQUE_CARD_TYPES_PERMITTED = 3 + + // Returns null if uniqueCards cannot be found + fun pick ( + cards: List, + npUsage: NPUsage = NPUsage.none, + npTypes: Map = emptyMap(), + braveChainEnum: BraveChainEnum = BraveChainEnum.None, + cardCountPerFieldSlotMap: Map? = null, + cardCountPerCardTypeMap: Map? = null, + forceBraveChain: Boolean = false, + ): List? { + // Try to ensure unknown is handled + val nonUnknownCards = AttackUtils.getValidNonUnknownCards(cards) + if (!AttackUtils.isChainable( + cards = nonUnknownCards, + npUsage = npUsage, + npTypes = npTypes, + )) { + return null + } + + val uniqueCardTypesFromNp = npTypes.values.toSet() + val cardCountPerCardTypeMap = cardCountPerCardTypeMap ?: AttackUtils.getCardsPerCardTypeMap(nonUnknownCards, npTypes) + if (!isMightyChainAllowed( + cards = nonUnknownCards, + npUsage = npUsage, + uniqueCardTypesFromNp = uniqueCardTypesFromNp, + npTypes = npTypes, + cardCountPerCardTypeMap = cardCountPerCardTypeMap + )) return null + + // Check for Brave Chain + val braveChainFieldSlot = AttackUtils.getBraveChainFieldSlot( + braveChainEnum = braveChainEnum, + cards = nonUnknownCards, + npUsage = npUsage, + ) + + val selectedCards = pick( + cards = nonUnknownCards, + npUsage = npUsage, + uniqueCardTypesAlreadyFilled = uniqueCardTypesFromNp, + braveChainFieldSlot = braveChainFieldSlot, + braveChainEnum = braveChainEnum, + forceBraveChain = forceBraveChain, + + cardCountPerFieldSlotMap = cardCountPerFieldSlotMap, + ) + + if (selectedCards == null) return null + // Handle for CardType.Unknown and ensure that 5 cards are always returned + return selectedCards + (cards - selectedCards) + } + + // Returns null if uniqueCards cannot be found + private fun pick( + cards: List, + npUsage: NPUsage = NPUsage.none, + // Decreases number of cards to get + // e.g. usually based on npSize + uniqueCardTypesAlreadyFilled: Set, + // In case of a Brave chain, we want to know what slot it is + braveChainFieldSlot: FieldSlot? = null, + braveChainEnum: BraveChainEnum = BraveChainEnum.None, + forceBraveChain: Boolean = false, + + cardCountPerFieldSlotMap: Map? = null, + ): List? { + val cardsToFind = TOTAL_UNIQUE_CARD_TYPES_PERMITTED + val uniqueCardTypes = uniqueCardTypesAlreadyFilled.toMutableSet() + + var selectedCards = mutableListOf() + // Cache the filteredList the first time, to make the next check faster + var cachedFilteredCards: List = cards + while (uniqueCardTypes.size < cardsToFind) { + var filteredCards = cachedFilteredCards.filter { + // Always look for a different card type + it.type !in uniqueCardTypes + // It should never reach here due to needing uniqueCardTypes, but just in case + && it !in selectedCards + } + cachedFilteredCards = filteredCards + + // If there is a braveChainFieldSlot, we want to try for a Brave Mighty Chain + if (braveChainFieldSlot != null && braveChainEnum != BraveChainEnum.Avoid) { + // Attempt to find one matching the fieldSlot + val fieldSlotList = filteredCards.filter { + it.fieldSlot == braveChainFieldSlot + } + // Even if it is empty, if BraveChainEnum.Always, + // it only accepts Brave Mighty Chains and not normal Mighty Chains + if (fieldSlotList.isNotEmpty() || forceBraveChain) filteredCards = fieldSlotList + } + val filteredCard = filteredCards.firstOrNull() + if (filteredCard == null) break // If cannot find, leave + uniqueCardTypes.add(filteredCard.type) + selectedCards.add(filteredCard) + } + + // If there isn't a valid list of cards, reject and return null + if (uniqueCardTypes.size < cardsToFind) return null + + if (braveChainEnum == BraveChainEnum.Avoid) { + val newSelection = getMightyChainWithoutBraveChain( + cards = cards, + npUsage = npUsage, + selectedCards = selectedCards.toList(), + cardCountPerFieldSlotMap = cardCountPerFieldSlotMap, + ) + if (newSelection == null) return null + // if it is not null, there is a valid non-Brave Chain option + selectedCards = newSelection.toMutableList() + } + + // Otherwise, we return the expected output + val remainder = cards - selectedCards + val combinedCards = selectedCards + remainder + + return combinedCards + } + + fun getMightyChainWithoutBraveChain ( + cards: List, + selectedCards: List, + npUsage: NPUsage = NPUsage.none, + cardCountPerFieldSlotMap: Map? = null, + ): List? { + val cardCountPerFieldSlotMap = cardCountPerFieldSlotMap ?: AttackUtils.getCardsPerFieldSlotMap(cards, npUsage) + // If there is only 1 unique field slot throughout, this is a valid entry (since it is impossible to avoid Brave Chain) + if (cardCountPerFieldSlotMap.size == 1) return selectedCards + + val allFieldSlots = selectedCards.mapNotNull { it.fieldSlot } + npUsage.nps.map { it.toFieldSlot() } + val uniqueFieldSlotsSet = allFieldSlots.toSet() + // If there is more than 1 unique fieldSlot, returning is fine + if (uniqueFieldSlotsSet.size > 1) return selectedCards + + // Otherwise, attempt to substitute cards + val newSelection = mutableListOf() + var isNewSelectionValid = false + for (card in selectedCards) { + // if already valid, add the card and continue + if (isNewSelectionValid) { + newSelection.add(card) + continue + } + val validReplacements = cards.filter{ + it.type == card.type + && it.fieldSlot != card.fieldSlot + } + if (validReplacements.isEmpty()) { + newSelection.add(card) + } else { + // Found a suitable replacement + newSelection.add(validReplacements.first()) + isNewSelectionValid = true + } + } + + // If a suitable replacement was found, return is allowed + if (isNewSelectionValid) { + return newSelection + } + + // Otherwise, return null + return null + } + + fun isMightyChainAllowed ( + cards: List, + npUsage: NPUsage, + npTypes: Map = emptyMap(), + uniqueCardTypesFromNp: Set? = null, + cardCountPerCardTypeMap: Map? = null, + ): Boolean { + val uniqueCardTypesFromNp = uniqueCardTypesFromNp ?: npTypes.values.toSet() + val cardCountPerCardTypeMap = cardCountPerCardTypeMap ?: AttackUtils.getCardsPerCardTypeMap(cards, npTypes) + val npUsageSize = npUsage.nps.size + + // Unable to make a Mighty Chain with Unknown cards in NP + if (npTypes.values.contains(CardTypeEnum.Unknown)) return false + + // if there are more than 1 NP and we don't know their types, + // just do the default since we can't guarantee a mighty chain anyway + if (npUsageSize > 0 && npTypes.size != npUsageSize) return false + + // Unable to make a Mighty Chain with Unknown cards in NP + if (npTypes.values.contains(CardTypeEnum.Unknown)) return false + + // Do not accept Unknown cards for Mighty Chain + if (cardCountPerCardTypeMap.getOrElse(CardTypeEnum.Unknown) { 0 } > 0) return false + + // If we do not have 3 unique cards at least, it is impossible to Mighty Chain + if (cardCountPerCardTypeMap.size < 3) return false + + // Success if + return ( + // number of nps is 1 + npUsageSize == 1 + // or number of nps do not match with unique size + // scenarios: + // 1 NP = no problem, we proceed + // 3 NP = Even if it doesn't match, it doesn't matter to us since all are filled + // 2 NP (different) = 2 unique == 2 nps (clear to proceed) + // 2 NP (same) = 2 clash already. No mighty chain here + || uniqueCardTypesFromNp.size == npUsageSize + ) + } +} \ No newline at end of file diff --git a/scripts/src/main/java/io/github/fate_grand_automata/scripts/prefs/IBattleConfig.kt b/scripts/src/main/java/io/github/fate_grand_automata/scripts/prefs/IBattleConfig.kt index 28c9ca345..360f2aeed 100644 --- a/scripts/src/main/java/io/github/fate_grand_automata/scripts/prefs/IBattleConfig.kt +++ b/scripts/src/main/java/io/github/fate_grand_automata/scripts/prefs/IBattleConfig.kt @@ -7,6 +7,7 @@ import io.github.fate_grand_automata.scripts.enums.ShuffleCardsEnum import io.github.fate_grand_automata.scripts.models.CardPriorityPerWave import io.github.fate_grand_automata.scripts.models.ServantPriorityPerWave import io.github.fate_grand_automata.scripts.models.ServantSpamConfig +import io.github.fate_grand_automata.scripts.models.battle.ChainPriorityPerWave interface IBattleConfig { val id: String @@ -15,6 +16,8 @@ interface IBattleConfig { var cardPriority: CardPriorityPerWave val useServantPriority: Boolean val servantPriority: ServantPriorityPerWave + val useChainPriority: Boolean + val chainPriority: ChainPriorityPerWave val rearrangeCards: List val braveChains: List val party: Int diff --git a/scripts/src/main/java/io/github/fate_grand_automata/scripts/prefs/IPreferences.kt b/scripts/src/main/java/io/github/fate_grand_automata/scripts/prefs/IPreferences.kt index 46df78165..a21a62e25 100644 --- a/scripts/src/main/java/io/github/fate_grand_automata/scripts/prefs/IPreferences.kt +++ b/scripts/src/main/java/io/github/fate_grand_automata/scripts/prefs/IPreferences.kt @@ -41,6 +41,10 @@ interface IPreferences { val stageCounterNew: Boolean val waitBeforeTurn: Duration val waitBeforeCards: Duration + val waitAfterOrderChange: Duration + + val npSpamCardDetectionSimilarity: Double + val npCardTypeSimilarity: Double val support: ISupportPreferencesCommon val platformPrefs: PlatformPrefs diff --git a/scripts/src/test/java/io/github/fate_grand_automata/scripts/BraveChainsTest.kt b/scripts/src/test/java/io/github/fate_grand_automata/scripts/BraveChainsTest.kt index 8173e5b5b..23f517c11 100644 --- a/scripts/src/test/java/io/github/fate_grand_automata/scripts/BraveChainsTest.kt +++ b/scripts/src/test/java/io/github/fate_grand_automata/scripts/BraveChainsTest.kt @@ -11,10 +11,8 @@ import kotlin.test.Test class BraveChainsTest { private fun shouldReturnSame(mode: BraveChainEnum) { - val braveChains = ApplyBraveChains() - val cards = FaceCardPriorityTest.lineup1 - val picked = braveChains.pick( + val picked = ApplyBraveChains.pick( cards = cards, braveChains = mode ) @@ -28,10 +26,8 @@ class BraveChainsTest { } private fun justRearrange(mode: BraveChainEnum) { - val braveChains = ApplyBraveChains() - val cards = FaceCardPriorityTest.lineup1 - val picked = braveChains.pick( + val picked = ApplyBraveChains.pick( cards = cards, braveChains = mode, rearrange = true @@ -57,10 +53,8 @@ class BraveChainsTest { @Test fun braveChainsWith1MatchingCard() { - val braveChains = ApplyBraveChains() - val cards = FaceCardPriorityTest.lineup1 - val picked = braveChains.pick( + val picked = ApplyBraveChains.pick( cards = cards, braveChains = BraveChainEnum.WithNP, npUsage = NPUsage(setOf(CommandCard.NP.A), 0) @@ -71,10 +65,8 @@ class BraveChainsTest { @Test fun braveChainsRearrangeWith1MatchingCard() { - val braveChains = ApplyBraveChains() - val cards = FaceCardPriorityTest.lineup1 - val picked = braveChains.pick( + val picked = ApplyBraveChains.pick( cards = cards, braveChains = BraveChainEnum.WithNP, rearrange = true, @@ -86,10 +78,8 @@ class BraveChainsTest { @Test fun braveChainsWith2MatchingCards() { - val braveChains = ApplyBraveChains() - val cards = FaceCardPriorityTest.lineup1 - val picked = braveChains.pick( + val picked = ApplyBraveChains.pick( cards = cards, braveChains = BraveChainEnum.WithNP, npUsage = NPUsage(setOf(CommandCard.NP.B), 0) @@ -100,10 +90,8 @@ class BraveChainsTest { @Test fun braveChainsRearrangeWith2MatchingCards() { - val braveChains = ApplyBraveChains() - val cards = FaceCardPriorityTest.lineup1 - val picked = braveChains.pick( + val picked = ApplyBraveChains.pick( cards = cards, braveChains = BraveChainEnum.WithNP, rearrange = true, @@ -115,10 +103,8 @@ class BraveChainsTest { @Test fun avoidBraveChains3DiffCards() { - val braveChains = ApplyBraveChains() - val cards = FaceCardPriorityTest.lineup1 - val picked = braveChains.pick( + val picked = ApplyBraveChains.pick( cards = cards, braveChains = BraveChainEnum.Avoid ).map { it.card } @@ -133,10 +119,8 @@ class BraveChainsTest { @Test fun avoidBraveChainsWith2DiffCards() { - val braveChains = ApplyBraveChains() - val cards = FaceCardPriorityTest.lineup2 - val picked = braveChains.pick( + val picked = ApplyBraveChains.pick( cards = cards, braveChains = BraveChainEnum.Avoid ).map { it.card } @@ -146,10 +130,8 @@ class BraveChainsTest { @Test fun avoidBraveChainsRearrangeWith2DiffCards() { - val braveChains = ApplyBraveChains() - val cards = FaceCardPriorityTest.lineup2 - val picked = braveChains.pick( + val picked = ApplyBraveChains.pick( cards = cards, braveChains = BraveChainEnum.Avoid, rearrange = true @@ -157,4 +139,4 @@ class BraveChainsTest { assertThat(picked).containsExactly(CommandCard.Face.A, CommandCard.Face.B, CommandCard.Face.E, CommandCard.Face.C, CommandCard.Face.D) } -} \ No newline at end of file +} diff --git a/scripts/src/test/java/io/github/fate_grand_automata/scripts/attack/AttackLineUps.kt b/scripts/src/test/java/io/github/fate_grand_automata/scripts/attack/AttackLineUps.kt new file mode 100644 index 000000000..d26e1ddaf --- /dev/null +++ b/scripts/src/test/java/io/github/fate_grand_automata/scripts/attack/AttackLineUps.kt @@ -0,0 +1,386 @@ +package io.github.fate_grand_automata.scripts.attack + +import io.github.fate_grand_automata.scripts.enums.CardAffinityEnum +import io.github.fate_grand_automata.scripts.enums.CardTypeEnum +import io.github.fate_grand_automata.scripts.models.CommandCard +import io.github.fate_grand_automata.scripts.models.FieldSlot +import io.github.fate_grand_automata.scripts.models.ParsedCard +import io.github.fate_grand_automata.scripts.models.TeamSlot + +class AttackLineUps { + data object Standard { + val scathach1WB = ParsedCard( + card = CommandCard.Face.A, + servant = TeamSlot.B, + fieldSlot = FieldSlot.B, + type = CardTypeEnum.Buster, + affinity = CardAffinityEnum.Weak + ) + val kama2Q = ParsedCard( + card = CommandCard.Face.B, + servant = TeamSlot.A, + fieldSlot = FieldSlot.A, + type = CardTypeEnum.Quick + ) + val nero3RA = ParsedCard( + card = CommandCard.Face.C, + servant = TeamSlot.C, + fieldSlot = FieldSlot.C, + type = CardTypeEnum.Arts, + affinity = CardAffinityEnum.Resist + ) + val nero4RA = ParsedCard( + card = CommandCard.Face.D, + servant = TeamSlot.C, + fieldSlot = FieldSlot.C, + type = CardTypeEnum.Arts, + affinity = CardAffinityEnum.Resist + ) + val scathach5WQ = ParsedCard( + card = CommandCard.Face.E, + servant = TeamSlot.B, + fieldSlot = FieldSlot.B, + type = CardTypeEnum.Quick, + affinity = CardAffinityEnum.Weak + ) + + /** + * Kama, Scathach, Nero; + * Scathach 1WB, Kama 2Q, Nero 3RA, Nero 4RA, Scathach 5WQ + */ + val lineup1 = listOf(scathach1WB, kama2Q, nero3RA, nero4RA, scathach5WQ) + + /** + * Kama, Scathach, Nero; + * Scathach 1WB, Scathach 5WQ, Kama 2Q, Nero 3RA, Nero 4RA + */ + val lineup2 = listOf(scathach1WB, scathach5WQ, kama2Q, nero3RA, nero4RA) + } + + data object MightyOutlier { + val scathach1WB = ParsedCard( + card = CommandCard.Face.A, + servant = TeamSlot.B, + fieldSlot = FieldSlot.B, + type = CardTypeEnum.Buster, + affinity = CardAffinityEnum.Weak + ) + val scathach2WA = ParsedCard( + card = CommandCard.Face.B, + servant = TeamSlot.B, + fieldSlot = FieldSlot.B, + type = CardTypeEnum.Arts, + affinity = CardAffinityEnum.Weak + ) + val nero3RA = ParsedCard( + card = CommandCard.Face.C, + servant = TeamSlot.C, + fieldSlot = FieldSlot.C, + type = CardTypeEnum.Arts, + affinity = CardAffinityEnum.Resist + ) + val nero4RA = ParsedCard( + card = CommandCard.Face.D, + servant = TeamSlot.C, + fieldSlot = FieldSlot.C, + type = CardTypeEnum.Arts, + affinity = CardAffinityEnum.Resist + ) + val scathach5WQ = ParsedCard( + card = CommandCard.Face.E, + servant = TeamSlot.B, + fieldSlot = FieldSlot.B, + type = CardTypeEnum.Quick, + affinity = CardAffinityEnum.Weak + ) + + /** + * Scathach, Nero; + * Scathach 1WB, Scathach 2Q, Nero 3RA, Nero 4RA, Scathach 5WQ + */ + val lineup1 = listOf(scathach1WB, scathach2WA, nero3RA, nero4RA, scathach5WQ) + val lineup1_DefaultMightyChain = listOf(scathach1WB, scathach2WA, scathach5WQ) + val lineup1_DefaultMightyChain_WithQuickNP = listOf(scathach1WB, scathach2WA) + val lineup1_DefaultMightyChain_WithNonScathachBusterNP = listOf(scathach2WA, scathach5WQ) + val lineup1_DefaultMightyChain_WithNonScathachArtsNP = listOf(scathach1WB, scathach5WQ) + + /** + * Scathach, Nero; + * Scathach 1WB, Scathach 5WQ, Scathach 2Q, Nero 3RA, Nero 4RA + */ + val lineup2 = listOf(scathach1WB, scathach5WQ, scathach2WA, nero3RA, nero4RA) + } + + data object BusterFocus { + val kiyohime1B = ParsedCard( + card = CommandCard.Face.A, + servant = TeamSlot.B, + fieldSlot = FieldSlot.B, + type = CardTypeEnum.Buster, + affinity = CardAffinityEnum.Weak + ) + val kiyohime2B = ParsedCard( + card = CommandCard.Face.B, + servant = TeamSlot.B, + fieldSlot = FieldSlot.B, + type = CardTypeEnum.Buster, + affinity = CardAffinityEnum.Weak + ) + val nero3RA = ParsedCard( + card = CommandCard.Face.C, + servant = TeamSlot.C, + fieldSlot = FieldSlot.C, + type = CardTypeEnum.Arts, + affinity = CardAffinityEnum.Resist + ) + val nero4RA = ParsedCard( + card = CommandCard.Face.D, + servant = TeamSlot.C, + fieldSlot = FieldSlot.C, + type = CardTypeEnum.Arts, + affinity = CardAffinityEnum.Resist + ) + val kiyohime5B = ParsedCard( + card = CommandCard.Face.E, + servant = TeamSlot.B, + fieldSlot = FieldSlot.B, + type = CardTypeEnum.Buster, + affinity = CardAffinityEnum.Weak + ) + val nero3BAlt = ParsedCard( + card = CommandCard.Face.E, + servant = TeamSlot.C, + fieldSlot = FieldSlot.C, + type = CardTypeEnum.Buster, + affinity = CardAffinityEnum.Resist + ) + val nero3QAlt = ParsedCard( + card = CommandCard.Face.E, + servant = TeamSlot.C, + fieldSlot = FieldSlot.C, + type = CardTypeEnum.Quick, + affinity = CardAffinityEnum.Resist + ) + val nero4BAlt = ParsedCard( + card = CommandCard.Face.D, + servant = TeamSlot.C, + fieldSlot = FieldSlot.C, + type = CardTypeEnum.Buster, + affinity = CardAffinityEnum.Resist + ) + val nero5BAlt = ParsedCard( + card = CommandCard.Face.E, + servant = TeamSlot.C, + fieldSlot = FieldSlot.C, + type = CardTypeEnum.Buster, + affinity = CardAffinityEnum.Resist + ) + + // Buster brave chain + /** kiyohime1B, kiyohime2B, nero3RA, nero4RA, kiyohime5B */ + val lineup01 = listOf(kiyohime1B, kiyohime2B, nero3RA, nero4RA, kiyohime5B) + /** kiyohime1B, kiyohime5B, kiyohime2B, nero3RA, nero4RA */ + val lineup02 = listOf(kiyohime1B, kiyohime5B, kiyohime2B, nero3RA, nero4RA) + + // Buster chain only + /** kiyohime1B, kiyohime2B, nero3RA, nero4RA, nero5BAlt */ + val lineup03 = listOf(kiyohime1B, kiyohime2B, nero3RA, nero4RA, nero5BAlt) + /** kiyohime1B, nero5BAlt, kiyohime2B, nero3RA, nero4RA */ + val lineup04 = listOf(kiyohime1B, nero5BAlt, kiyohime2B, nero3RA, nero4RA) + + // Buster chain with either Brave or non-Brave + /** kiyohime1B, kiyohime2B, nero3RA, nero4BAlt, kiyohime5B */ + val lineup05 = listOf(kiyohime1B, kiyohime2B, nero3RA, nero4BAlt, kiyohime5B) + /** kiyohime1B, kiyohime5B, kiyohime2B, nero3RA, nero4BAlt */ + val lineup06 = listOf(kiyohime1B, kiyohime5B, kiyohime2B, nero3RA, nero4BAlt) + + // Buster brave chain or Mighty chain + /** kiyohime1B, kiyohime2B, nero3QAlt, nero4RA, kiyohime5B */ + val lineup07 = listOf(kiyohime1B, kiyohime2B, nero3QAlt, nero4RA, kiyohime5B) + /** kiyohime1B, kiyohime5B, kiyohime2B, nero3QAlt, nero4RA */ + val lineup08 = listOf(kiyohime1B, kiyohime5B, kiyohime2B, nero3QAlt, nero4RA) + + // Buster chain or Mighty brave chain + /** kiyohime1B, kiyohime2B, nero3QAlt, nero4RA, nero5BAlt */ + val lineup09 = listOf(kiyohime1B, kiyohime2B, nero3QAlt, nero4RA, nero5BAlt) + /** kiyohime1B, nero5BAlt, kiyohime2B, nero3QAlt, nero4RA */ + val lineup10 = listOf(kiyohime1B, nero5BAlt, kiyohime2B, nero3QAlt, nero4RA) + + // Avoid chain + /** kiyohime1B, kiyohime2B, nero3BAlt, nero4BAlt, kiyohime5B */ + val lineup11 = listOf(kiyohime1B, kiyohime2B, nero3BAlt, nero4BAlt, kiyohime5B) + } + + data object SingleServantOnly { + val scathach1WB = ParsedCard( + card = CommandCard.Face.A, + servant = TeamSlot.B, + fieldSlot = FieldSlot.B, + type = CardTypeEnum.Buster, + affinity = CardAffinityEnum.Weak + ) + val scathach2WQ = ParsedCard( + card = CommandCard.Face.B, + servant = TeamSlot.B, + fieldSlot = FieldSlot.B, + type = CardTypeEnum.Quick, + affinity = CardAffinityEnum.Weak + ) + val scathach3WA = ParsedCard( + card = CommandCard.Face.C, + servant = TeamSlot.B, + fieldSlot = FieldSlot.B, + type = CardTypeEnum.Arts, + affinity = CardAffinityEnum.Weak + ) + val scathach3WAltQ = ParsedCard( + card = CommandCard.Face.C, + servant = TeamSlot.B, + fieldSlot = FieldSlot.B, + type = CardTypeEnum.Quick, + affinity = CardAffinityEnum.Weak + ) + val scathach4WB = ParsedCard( + card = CommandCard.Face.D, + servant = TeamSlot.B, + fieldSlot = FieldSlot.B, + type = CardTypeEnum.Buster, + affinity = CardAffinityEnum.Weak + ) + val scathach5WQ = ParsedCard( + card = CommandCard.Face.E, + servant = TeamSlot.B, + fieldSlot = FieldSlot.B, + type = CardTypeEnum.Quick, + affinity = CardAffinityEnum.Weak + ) + val scathach5WAAlt = ParsedCard( + card = CommandCard.Face.E, + servant = TeamSlot.B, + fieldSlot = FieldSlot.B, + type = CardTypeEnum.Arts, + affinity = CardAffinityEnum.Weak + ) + + /** scathach1WB, scathach2WQ, scathach3WA, scathach4WB, scathach5WQ */ + val lineup1 = listOf(scathach1WB, scathach2WQ, scathach3WA, scathach4WB, scathach5WQ) + + /** scathach1WB, scathach4WB, scathach5WQ, scathach2WQ, scathach3WA */ + val lineup2 = listOf(scathach1WB, scathach4WB, scathach5WQ, scathach2WQ, scathach3WA) + + /** scathach5WQ, scathach2WQ, scathach3WA, scathach1WB, scathach4WB */ + val lineup3 = listOf(scathach5WQ, scathach2WQ, scathach3WA, scathach1WB, scathach4WB) + + // 3 Quick, 2 Buster; Buster starting card + /** servant1WB, servant2WQ, servant3WAltQ, servant4WB, servant5WQ */ + val lineup4 = listOf(scathach1WB, scathach2WQ, scathach3WAltQ, scathach4WB, scathach5WQ) + // 3 Quick, 2 Buster; Quick starting card + /** servant5WQ, servant2WQ, servant3WAltQ, servant1WB, servant4WB */ + val lineup5 = listOf(scathach5WQ, scathach2WQ, scathach3WAltQ, scathach1WB, scathach4WB) + + /** scathach3WA, scathach5WAAlt, scathach1WB, scathach4WB, scathach2WQ */ + val lineup6_special = listOf(scathach3WA, scathach5WAAlt, scathach1WB, scathach4WB, scathach2WQ) + } + + data object Unknown { + val scathach1Unknown = ParsedCard( + card = CommandCard.Face.A, + servant = TeamSlot.B, + fieldSlot = FieldSlot.B, + type = CardTypeEnum.Unknown, + affinity = CardAffinityEnum.Weak + ) + val kama2Unknown = ParsedCard( + card = CommandCard.Face.B, + servant = TeamSlot.A, + fieldSlot = FieldSlot.A, + type = CardTypeEnum.Unknown + ) + val nero3Unknown = ParsedCard( + card = CommandCard.Face.C, + servant = TeamSlot.C, + fieldSlot = FieldSlot.C, + type = CardTypeEnum.Unknown, + affinity = CardAffinityEnum.Resist + ) + val nero4Unknown = ParsedCard( + card = CommandCard.Face.D, + servant = TeamSlot.C, + fieldSlot = FieldSlot.C, + type = CardTypeEnum.Unknown, + affinity = CardAffinityEnum.Resist + ) + val scathach5Unknown = ParsedCard( + card = CommandCard.Face.E, + servant = TeamSlot.B, + fieldSlot = FieldSlot.B, + type = CardTypeEnum.Unknown, + affinity = CardAffinityEnum.Weak + ) + + /** + * Standard.lineup1 with 1st card Unknown + */ + val lineup1 = listOf( + scathach1Unknown, + Standard.kama2Q, + Standard.nero3RA, + Standard.nero4RA, + Standard.scathach5WQ, + ) + + /** + * Standard.lineup1 with 2 cards Unknown + */ + val lineup2 = listOf( + scathach1Unknown, + kama2Unknown, + Standard.nero3RA, + Standard.nero4RA, + Standard.scathach5WQ, + ) + + /** + * Standard.lineup1 with 3 cards Unknown + */ + val lineup3 = listOf( + scathach1Unknown, + kama2Unknown, + nero3Unknown, + Standard.nero4RA, + Standard.scathach5WQ, + ) + + /** + * All cards Unknown + */ + val lineup4 = listOf( + scathach1Unknown, + kama2Unknown, + nero3Unknown, + nero4Unknown, + scathach5Unknown, + ) + + /** + * Special option for Buster/Brave Chain. 1 Unknown + */ + val lineup5 = listOf( + BusterFocus.kiyohime1B, + BusterFocus.kiyohime2B, + nero3Unknown, + BusterFocus.nero4RA, + BusterFocus.kiyohime5B, + ) + /** + * Special option for Buster/Brave Chain. 2 Unknown, only valid BraveChain with NP. + */ + val lineup6 = listOf( + scathach1Unknown, + BusterFocus.kiyohime2B, + nero3Unknown, + BusterFocus.nero4RA, + BusterFocus.kiyohime5B, + ) + } +} \ No newline at end of file diff --git a/scripts/src/test/java/io/github/fate_grand_automata/scripts/attack/AttackUtilsTest.kt b/scripts/src/test/java/io/github/fate_grand_automata/scripts/attack/AttackUtilsTest.kt new file mode 100644 index 000000000..fa6e51f32 --- /dev/null +++ b/scripts/src/test/java/io/github/fate_grand_automata/scripts/attack/AttackUtilsTest.kt @@ -0,0 +1,752 @@ +package io.github.fate_grand_automata.scripts.attack + +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isNull +import io.github.fate_grand_automata.scripts.enums.BraveChainEnum +import io.github.fate_grand_automata.scripts.enums.CardTypeEnum +import io.github.fate_grand_automata.scripts.models.CommandCard +import io.github.fate_grand_automata.scripts.models.FieldSlot +import io.github.fate_grand_automata.scripts.models.NPUsage +import io.github.fate_grand_automata.scripts.modules.attack.AttackUtils +import kotlin.test.Test + +class AttackUtilsTest { + val braveChainEnums = BraveChainEnum.entries + + @Test + fun `getCardsPerFieldSlotMap, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ)`() { + val cards = AttackLineUps.Standard.lineup1 + val result = AttackUtils.getCardsPerFieldSlotMap( + cards = cards, + ) + + assertThat(result.size, "Result.size").isEqualTo(3) + assertThat(result[FieldSlot.A], "FieldSlot.A").isEqualTo(1) + assertThat(result[FieldSlot.B], "FieldSlot.B").isEqualTo(2) + assertThat(result[FieldSlot.C], "FieldSlot.C").isEqualTo(2) + } + + @Test + fun `getCardsPerFieldSlotMap, Standard - lineup2 (1SB,5SQ,2KQ,3NA,4NA)`() { + val cards = AttackLineUps.Standard.lineup2 + val result = AttackUtils.getCardsPerFieldSlotMap( + cards = cards, + ) + + assertThat(result.size, "Result.size").isEqualTo(3) + assertThat(result[FieldSlot.A], "FieldSlot.A").isEqualTo(1) + assertThat(result[FieldSlot.B], "FieldSlot.B").isEqualTo(2) + assertThat(result[FieldSlot.C], "FieldSlot.C").isEqualTo(2) + } + + @Test + fun `getCardsPerFieldSlotMap, BusterFocus - lineup01 (1KB,2KB,3NA,4NA,5KB)`() { + val cards = AttackLineUps.BusterFocus.lineup01 + val result = AttackUtils.getCardsPerFieldSlotMap( + cards = cards, + ) + + assertThat(result.size, "Result.size").isEqualTo(2) + assertThat(result[FieldSlot.B], "FieldSlot.B").isEqualTo(3) + assertThat(result[FieldSlot.C], "FieldSlot.C").isEqualTo(2) + } + + @Test + fun `getCardsPerFieldSlotMap, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 1-KamaNP`() { + val cards = AttackLineUps.Standard.lineup1 + val result = AttackUtils.getCardsPerFieldSlotMap( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0) + ) + + assertThat(result.size, "Result.size").isEqualTo(3) + assertThat(result[FieldSlot.A], "FieldSlot.A").isEqualTo(2) + assertThat(result[FieldSlot.B], "FieldSlot.B").isEqualTo(2) + assertThat(result[FieldSlot.C], "FieldSlot.C").isEqualTo(2) + } + + @Test + fun `getCardsPerFieldSlotMap, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2-ScathachNP`() { + val cards = AttackLineUps.Standard.lineup1 + val result = AttackUtils.getCardsPerFieldSlotMap( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0) + ) + + assertThat(result.size, "Result.size").isEqualTo(3) + assertThat(result[FieldSlot.A], "FieldSlot.A").isEqualTo(1) + assertThat(result[FieldSlot.B], "FieldSlot.B").isEqualTo(3) + assertThat(result[FieldSlot.C], "FieldSlot.C").isEqualTo(2) + } + + @Test + fun `getCardsPerFieldSlotMap, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 3-NeroNP`() { + val cards = AttackLineUps.Standard.lineup1 + val result = AttackUtils.getCardsPerFieldSlotMap( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0) + ) + + assertThat(result.size, "Result.size").isEqualTo(3) + assertThat(result[FieldSlot.A], "FieldSlot.A").isEqualTo(1) + assertThat(result[FieldSlot.B], "FieldSlot.B").isEqualTo(2) + assertThat(result[FieldSlot.C], "FieldSlot.C").isEqualTo(3) + } + + @Test + fun `getFieldSlotsWithValidBraveChain, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ)`() { + val cards = AttackLineUps.Standard.lineup1 + val result = AttackUtils.getFieldSlotsWithValidBraveChain( + cards = cards, + ) + + assertThat(result.size).isEqualTo(0) + } + + @Test + fun `getFieldSlotsWithValidBraveChain, Standard - lineup2 (1SB,5SQ,2KQ,3NA,4NA)`() { + val cards = AttackLineUps.Standard.lineup2 + val result = AttackUtils.getFieldSlotsWithValidBraveChain( + cards = cards, + ) + + assertThat(result.size).isEqualTo(0) + } + + @Test + fun `getFieldSlotsWithValidBraveChain, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 1-KamaNP`() { + val cards = AttackLineUps.Standard.lineup1 + val result = AttackUtils.getFieldSlotsWithValidBraveChain( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0) + ) + + assertThat(result.size).isEqualTo(0) + } + + @Test + fun `getFieldSlotsWithValidBraveChain, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2-ScathachNP`() { + val cards = AttackLineUps.Standard.lineup1 + val result = AttackUtils.getFieldSlotsWithValidBraveChain( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0) + ) + + assertThat(result.size, "Result.size").isEqualTo(1) + assertThat(result[0], "FieldSlotB").isEqualTo(FieldSlot.B) + } + + @Test + fun `getFieldSlotsWithValidBraveChain, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 3-NeroNP`() { + val cards = AttackLineUps.Standard.lineup1 + val result = AttackUtils.getFieldSlotsWithValidBraveChain( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0) + ) + + assertThat(result.size, "Result.size").isEqualTo(1) + assertThat(result[0], "FieldSlotC").isEqualTo(FieldSlot.C) + } + + @Test + fun `getFieldSlotsWithValidBraveChain, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 1-KamaNP + 2-ScathachNP`() { + val cards = AttackLineUps.Standard.lineup1 + val result = AttackUtils.getFieldSlotsWithValidBraveChain( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B), 0) + ) + + assertThat(result.size, "Result.size").isEqualTo(0) + } + + @Test + fun `getFieldSlotsWithValidBraveChain, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + All 3 NP`() { + val cards = AttackLineUps.Standard.lineup1 + val result = AttackUtils.getFieldSlotsWithValidBraveChain( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B, CommandCard.NP.C), 0) + ) + + assertThat(result.size, "Result.size").isEqualTo(0) + } + + @Test + fun `getCardsPerCardTypeMap, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ)`() { + val cards = AttackLineUps.Standard.lineup1 + val result = AttackUtils.getCardsPerCardTypeMap( + cards = cards, + ) + + assertThat(result.size).isEqualTo(3) + assertThat(result[CardTypeEnum.Arts], "Arts").isEqualTo(2) + assertThat(result[CardTypeEnum.Quick], "Quick").isEqualTo(2) + assertThat(result[CardTypeEnum.Buster], "Buster").isEqualTo(1) + } + + @Test + fun `getCardsPerCardTypeMap, Standard - lineup2 (1SB,5SQ,2KQ,3NA,4NA)`() { + val cards = AttackLineUps.Standard.lineup2 + val result = AttackUtils.getCardsPerCardTypeMap( + cards = cards, + ) + + assertThat(result.size).isEqualTo(3) + assertThat(result[CardTypeEnum.Arts], "Arts").isEqualTo(2) + assertThat(result[CardTypeEnum.Quick], "Quick").isEqualTo(2) + assertThat(result[CardTypeEnum.Buster], "Buster").isEqualTo(1) + } + + @Test + fun `getCardsPerCardTypeMap, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 1-KamaNP`() { + val cards = AttackLineUps.Standard.lineup1 + val result = AttackUtils.getCardsPerCardTypeMap( + cards = cards, + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick + ) + ) + + assertThat(result.size).isEqualTo(3) + assertThat(result[CardTypeEnum.Arts], "Arts").isEqualTo(2) + assertThat(result[CardTypeEnum.Quick], "Quick").isEqualTo(3) + assertThat(result[CardTypeEnum.Buster], "Buster").isEqualTo(1) + } + + @Test + fun `getCardsPerCardTypeMap, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2-ScathachNP`() { + val cards = AttackLineUps.Standard.lineup1 + val result = AttackUtils.getCardsPerCardTypeMap( + cards = cards, + npTypes = mapOf( + FieldSlot.B to CardTypeEnum.Quick + ) + ) + + assertThat(result.size, "Result.size").isEqualTo(3) + assertThat(result[CardTypeEnum.Arts], "Arts").isEqualTo(2) + assertThat(result[CardTypeEnum.Quick], "Quick").isEqualTo(3) + assertThat(result[CardTypeEnum.Buster], "Buster").isEqualTo(1) + } + + @Test + fun `getCardsPerCardTypeMap, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 3-NeroNP`() { + val cards = AttackLineUps.Standard.lineup1 + val result = AttackUtils.getCardsPerCardTypeMap( + cards = cards, + npTypes = mapOf( + FieldSlot.C to CardTypeEnum.Arts + ) + ) + + assertThat(result.size, "Result.size").isEqualTo(3) + assertThat(result[CardTypeEnum.Arts], "Arts").isEqualTo(3) + assertThat(result[CardTypeEnum.Quick], "Quick").isEqualTo(2) + assertThat(result[CardTypeEnum.Buster], "Buster").isEqualTo(1) + } + + @Test + fun `getCardsPerCardTypeMap, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 1-KamaNP + 2-ScathachNP`() { + val cards = AttackLineUps.Standard.lineup1 + val result = AttackUtils.getCardsPerCardTypeMap( + cards = cards, + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick, + FieldSlot.B to CardTypeEnum.Quick + ) + ) + + assertThat(result.size, "Result.size").isEqualTo(3) + assertThat(result[CardTypeEnum.Arts], "Arts").isEqualTo(2) + assertThat(result[CardTypeEnum.Quick], "Quick").isEqualTo(4) + assertThat(result[CardTypeEnum.Buster], "Buster").isEqualTo(1) + } + + @Test + fun `getCardsPerCardTypeMap, BusterFocus - lineup01 (1KB,2KB,3NA,4NA,5KB)`() { + val cards = AttackLineUps.BusterFocus.lineup01 + val result = AttackUtils.getCardsPerCardTypeMap( + cards = cards, + ) + + assertThat(result.size, "Result.size").isEqualTo(2) + assertThat(result[CardTypeEnum.Arts], "Arts").isEqualTo(2) + assertThat(result[CardTypeEnum.Quick], "Quick").isEqualTo(null) + assertThat(result[CardTypeEnum.Buster], "Buster").isEqualTo(3) + } + + @Test + fun `getBraveChainFieldSlot, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ)`() { + for (braveChainEnum in braveChainEnums) { + val cards = AttackLineUps.Standard.lineup1 + val result = AttackUtils.getBraveChainFieldSlot( + cards = cards, + braveChainEnum = braveChainEnum, + ) + + assertThat(result, braveChainEnum.toString()).isNull() + } + } + + @Test + fun `getBraveChainFieldSlot, Standard - lineup2 (1SB,5SQ,2KQ,3NA,4NA)`() { + for (braveChainEnum in braveChainEnums) { + val cards = AttackLineUps.Standard.lineup2 + val result = AttackUtils.getBraveChainFieldSlot( + cards = cards, + braveChainEnum = braveChainEnum, + ) + + assertThat(result, braveChainEnum.toString()).isNull() + } + } + + @Test + fun `getBraveChainFieldSlot, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 1-KamaNP`() { + for (braveChainEnum in braveChainEnums) { + val cards = AttackLineUps.Standard.lineup2 + val result = AttackUtils.getBraveChainFieldSlot( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + braveChainEnum = braveChainEnum, + ) + + assertThat(result, braveChainEnum.toString()).isNull() + } + } + + @Test + fun `getBraveChainFieldSlot, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2-ScathachNP`() { + for (braveChainEnum in braveChainEnums) { + val cards = AttackLineUps.Standard.lineup1 + val result = AttackUtils.getBraveChainFieldSlot( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + braveChainEnum = braveChainEnum, + ) + + when (braveChainEnum) { + BraveChainEnum.WithNP, + BraveChainEnum.Always, -> assertThat(result, braveChainEnum.toString()) + .isEqualTo(FieldSlot.B) + else -> assertThat(result, braveChainEnum.toString()).isNull() + } + } + } + + @Test + fun `getBraveChainFieldSlot, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 3-NeroNP`() { + for (braveChainEnum in braveChainEnums) { + val cards = AttackLineUps.Standard.lineup1 + val result = AttackUtils.getBraveChainFieldSlot( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + braveChainEnum = braveChainEnum, + ) + + when (braveChainEnum) { + BraveChainEnum.WithNP, + BraveChainEnum.Always, -> assertThat(result, braveChainEnum.toString()) + .isEqualTo(FieldSlot.C) + else -> assertThat(result, braveChainEnum.toString()).isNull() + } + } + } + + @Test + fun `getBraveChainFieldSlot, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 1-KamaNP + 2-ScathachNP`() { + for (braveChainEnum in braveChainEnums) { + val cards = AttackLineUps.Standard.lineup1 + val result = AttackUtils.getBraveChainFieldSlot( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B), 0), + braveChainEnum = braveChainEnum, + ) + + assertThat(result, braveChainEnum.toString()).isNull() + } + } + + @Test + fun `getBraveChainFieldSlot, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + All 3 NP`() { + for (braveChainEnum in braveChainEnums) { + val cards = AttackLineUps.Standard.lineup1 + val result = AttackUtils.getBraveChainFieldSlot( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B, CommandCard.NP.C), 0), + braveChainEnum = braveChainEnum, + ) + + assertThat(result, braveChainEnum.toString()).isNull() + } + } + + @Test + fun `getBraveChainFieldSlot, BusterFocus - lineup01 (1KB,2KB,3NA,4NA,5KB)`() { + for (braveChainEnum in braveChainEnums) { + val cards = AttackLineUps.BusterFocus.lineup01 + val result = AttackUtils.getBraveChainFieldSlot( + cards = cards, + braveChainEnum = braveChainEnum, + ) + + when (braveChainEnum) { + BraveChainEnum.Always -> assertThat(result, braveChainEnum.toString()) + .isEqualTo(FieldSlot.B) + else -> assertThat(result, braveChainEnum.toString()).isNull() + } + } + } + + @Test + fun `getBraveChainFieldSlot, BusterFocus - lineup03 (1KB,2KB,3NA,4NA,5NB)`() { + for (braveChainEnum in braveChainEnums) { + val cards = AttackLineUps.BusterFocus.lineup03 + val result = AttackUtils.getBraveChainFieldSlot( + cards = cards, + braveChainEnum = braveChainEnum, + ) + + when (braveChainEnum) { + BraveChainEnum.Always -> assertThat(result, braveChainEnum.toString()) + .isEqualTo(FieldSlot.C) + else -> assertThat(result, braveChainEnum.toString()).isNull() + } + } + } + + @Test + fun `getBraveChainFieldSlot, BusterFocus - lineup03 (1KB,2KB,3NA,4NA,5NB) - 2Kiyo-NP`() { + for (braveChainEnum in braveChainEnums) { + val cards = AttackLineUps.BusterFocus.lineup03 + val result = AttackUtils.getBraveChainFieldSlot( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + braveChainEnum = braveChainEnum, + ) + + when (braveChainEnum) { + BraveChainEnum.WithNP, + BraveChainEnum.Always, -> assertThat(result, braveChainEnum.toString()) + .isEqualTo(FieldSlot.B) + else -> assertThat(result, braveChainEnum.toString()).isNull() + } + } + } + + @Test + fun `getBraveChainFieldSlot, BusterFocus - lineup03 (1KB,2KB,3NA,4NA,5NB) - 3Nero-NP`() { + for (braveChainEnum in braveChainEnums) { + val cards = AttackLineUps.BusterFocus.lineup03 + val result = AttackUtils.getBraveChainFieldSlot( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + braveChainEnum = braveChainEnum, + ) + + when (braveChainEnum) { + BraveChainEnum.WithNP, + BraveChainEnum.Always, -> assertThat(result, braveChainEnum.toString()) + .isEqualTo(FieldSlot.C) + else -> assertThat(result, braveChainEnum.toString()).isNull() + } + } + } + + @Test + fun `getValidNonUnknownCards, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ)`() { + val cards = AttackLineUps.Standard.lineup1 + val result = AttackUtils.getValidNonUnknownCards( + cards = cards, + ) + + assertThat(result.size).isEqualTo(5) + } + + @Test + fun `getValidNonUnknownCards, Unknown - lineup1 (1 Unknown)`() { + val cards = AttackLineUps.Unknown.lineup1 + val result = AttackUtils.getValidNonUnknownCards( + cards = cards, + ) + + assertThat(result.size).isEqualTo(4) + } + + @Test + fun `getValidNonUnknownCards, Unknown - lineup2 (2 Unknown)`() { + val cards = AttackLineUps.Unknown.lineup2 + val result = AttackUtils.getValidNonUnknownCards( + cards = cards, + ) + + assertThat(result.size).isEqualTo(3) + } + + @Test + fun `getValidNonUnknownCards, Unknown - lineup3 (3 Unknown)`() { + val cards = AttackLineUps.Unknown.lineup3 + val result = AttackUtils.getValidNonUnknownCards( + cards = cards, + ) + + assertThat(result.size).isEqualTo(2) + } + + @Test + fun `getValidNonUnknownCards, Unknown - lineup4 (5 Unknown)`() { + val cards = AttackLineUps.Unknown.lineup4 + val result = AttackUtils.getValidNonUnknownCards( + cards = cards, + ) + + assertThat(result.size).isEqualTo(0) + } + + @Test + fun `isChainable, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ)`() { + val cards = AttackLineUps.Standard.lineup1 + val result = AttackUtils.isChainable( + cards = cards, + ) + + assertThat(result).isEqualTo(true) + } + + @Test + fun `isChainable, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 1 Valid NP`() { + val cards = AttackLineUps.Standard.lineup1 + val result = AttackUtils.isChainable( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + ) + + assertThat(result).isEqualTo(true) + } + + @Test + fun `isChainable, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 1 Valid NP, with npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + val result = AttackUtils.isChainable( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick + ), + ) + + assertThat(result).isEqualTo(true) + } + + @Test + fun `isChainable, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 1 Unknown NP`() { + val cards = AttackLineUps.Standard.lineup1 + val result = AttackUtils.isChainable( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Unknown + ), + ) + + assertThat(result).isEqualTo(false) + } + + @Test + fun `isChainable, Unknown - lineup1 (1 Unknown)`() { + val cards = AttackLineUps.Unknown.lineup1 + val result = AttackUtils.isChainable( + cards = cards, + ) + + assertThat(result).isEqualTo(true) + } + + @Test + fun `isChainable, Unknown - lineup1 (1 Unknown) + 1 Valid NP`() { + val cards = AttackLineUps.Unknown.lineup1 + val result = AttackUtils.isChainable( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + ) + + assertThat(result).isEqualTo(true) + } + + @Test + fun `isChainable, Unknown - lineup1 (1 Unknown) + 1 Valid NP, with npTypes`() { + val cards = AttackLineUps.Unknown.lineup1 + val result = AttackUtils.isChainable( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick + ), + ) + + assertThat(result).isEqualTo(true) + } + + @Test + fun `isChainable, Unknown - lineup1 (1 Unknown) + 1 Unknown NP, with npTypes`() { + val cards = AttackLineUps.Unknown.lineup1 + val result = AttackUtils.isChainable( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Unknown + ), + ) + + assertThat(result).isEqualTo(false) + } + + @Test + fun `isChainable, Unknown - lineup2 (2 Unknown)`() { + val cards = AttackLineUps.Unknown.lineup2 + val result = AttackUtils.isChainable( + cards = cards, + ) + + assertThat(result).isEqualTo(true) + } + + @Test + fun `isChainable, Unknown - lineup3 (3 Unknown)`() { + val cards = AttackLineUps.Unknown.lineup3 + val result = AttackUtils.isChainable( + cards = cards, + ) + + assertThat(result).isEqualTo(false) + } + + @Test + fun `isChainable, Unknown - lineup3 (3 Unknown) + Valid NP`() { + val cards = AttackLineUps.Unknown.lineup3 + val result = AttackUtils.isChainable( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + ) + + assertThat(result).isEqualTo(true) + } + + @Test + fun `isChainable, Unknown - lineup3 (3 Unknown) + Valid NP, with npTypes`() { + val cards = AttackLineUps.Unknown.lineup3 + val result = AttackUtils.isChainable( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick + ), + ) + + assertThat(result).isEqualTo(true) + } + + @Test + fun `isChainable, Unknown - lineup4 (5 Unknown)`() { + val cards = AttackLineUps.Unknown.lineup4 + val result = AttackUtils.isChainable( + cards = cards, + ) + + assertThat(result).isEqualTo(false) + } + + @Test + fun `isChainable, Unknown - lineup4 (5 Unknown) + 1 Valid NP`() { + val cards = AttackLineUps.Unknown.lineup4 + val result = AttackUtils.isChainable( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + ) + + assertThat(result).isEqualTo(false) + } + + @Test + fun `isChainable, Unknown - lineup4 (5 Unknown) + 1 Valid NP, with npTypes`() { + val cards = AttackLineUps.Unknown.lineup4 + val result = AttackUtils.isChainable( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick + ), + ) + + assertThat(result).isEqualTo(false) + } + + @Test + fun `isChainable, Unknown - lineup4 (5 Unknown) + 2 Valid NP`() { + val cards = AttackLineUps.Unknown.lineup4 + val result = AttackUtils.isChainable( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B), 0), + ) + + assertThat(result).isEqualTo(false) + } + + @Test + fun `isChainable, Unknown - lineup4 (5 Unknown) + 2 Valid NP, with npTypes`() { + val cards = AttackLineUps.Unknown.lineup4 + val result = AttackUtils.isChainable( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick, + FieldSlot.B to CardTypeEnum.Quick, + ), + ) + + assertThat(result).isEqualTo(false) + } + + @Test + fun `isChainable, Unknown - lineup4 (5 Unknown) + 3 Valid NP`() { + val cards = AttackLineUps.Unknown.lineup4 + val result = AttackUtils.isChainable( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B, CommandCard.NP.C), 0), + ) + + assertThat(result).isEqualTo(true) + } + + @Test + fun `isChainable, Unknown - lineup4 (5 Unknown) + 3 Valid NP, with npTypes`() { + val cards = AttackLineUps.Unknown.lineup4 + val result = AttackUtils.isChainable( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B, CommandCard.NP.C), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick, + FieldSlot.B to CardTypeEnum.Quick, + FieldSlot.B to CardTypeEnum.Arts, + ), + ) + + assertThat(result).isEqualTo(true) + } + + @Test + fun `isChainable, Unknown - lineup4 (5 Unknown) + 2 Valid NP + 1 Unknown NP, with npTypes`() { + val cards = AttackLineUps.Unknown.lineup4 + val result = AttackUtils.isChainable( + cards = cards, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B, CommandCard.NP.C), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick, + FieldSlot.B to CardTypeEnum.Quick, + FieldSlot.B to CardTypeEnum.Unknown, + ), + ) + + assertThat(result).isEqualTo(false) + } +} \ No newline at end of file diff --git a/scripts/src/test/java/io/github/fate_grand_automata/scripts/attack/AvoidChainTest.kt b/scripts/src/test/java/io/github/fate_grand_automata/scripts/attack/AvoidChainTest.kt new file mode 100644 index 000000000..d871ba7ce --- /dev/null +++ b/scripts/src/test/java/io/github/fate_grand_automata/scripts/attack/AvoidChainTest.kt @@ -0,0 +1,634 @@ +package io.github.fate_grand_automata.scripts.attack + +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.isEmpty +import io.github.fate_grand_automata.scripts.enums.BraveChainEnum +import io.github.fate_grand_automata.scripts.enums.CardTypeEnum +import io.github.fate_grand_automata.scripts.models.CommandCard +import io.github.fate_grand_automata.scripts.models.FieldSlot +import io.github.fate_grand_automata.scripts.models.NPUsage +import kotlin.test.Test +import io.github.fate_grand_automata.scripts.modules.attack.AvoidChainHandler + +class AvoidChainTest { + val braveChainEnums = BraveChainEnum.entries + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ)`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.E, + CommandCard.Face.C, + CommandCard.Face.D, + ) + } + } + + @Test + fun `Standard - lineup2 (1SB,5SQ,2KQ,3NA,4NA)`() { + val cards = AttackLineUps.Standard.lineup2 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.E, + CommandCard.Face.C, + CommandCard.Face.D, + ) + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 1Kama-NP`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.E, + ) + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 1Kama-NP, with npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick + ) + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.E, + ) + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2Scathach-NP, with npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.B to CardTypeEnum.Quick + ) + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Always, + BraveChainEnum.WithNP -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.E, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + ) + else -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.E, + ) + } + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 3Nero-NP, with npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + npTypes = mapOf( + FieldSlot.C to CardTypeEnum.Arts + ) + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Always, + BraveChainEnum.WithNP -> + assertThat(picked, braveChainEnum.toString()).isEmpty() + else -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.B, + CommandCard.Face.D, + CommandCard.Face.E, + ) + } + + } + } + + @Test + fun `BusterFocus - lineup01 (1KB,2KB,3NA,4NA,5KB)`() { + val cards = AttackLineUps.BusterFocus.lineup01 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + + when (braveChainEnum) { + BraveChainEnum.Always -> + assertThat(picked, braveChainEnum.toString()).isEmpty() + else -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.B, + CommandCard.Face.D, + CommandCard.Face.E, + ) + } + } + } + + @Test + fun `BusterFocus - lineup01 (1KB,2KB,3NA,4NA,5KB) + 1 NP (2Kiyohime)`() { + val cards = AttackLineUps.BusterFocus.lineup01 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.B to CardTypeEnum.Buster + ), + )?.map { it.card } ?: emptyList() + + + when (braveChainEnum) { + BraveChainEnum.Always, + BraveChainEnum.WithNP -> + assertThat(picked, braveChainEnum.toString()).isEmpty() + else -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.B, + CommandCard.Face.D, + CommandCard.Face.E, + ) + } + } + } + + @Test + fun `BusterFocus - lineup11 (1KB,2KB,3NB,4NB,5KB)`() { + val cards = AttackLineUps.BusterFocus.lineup11 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.B to CardTypeEnum.Buster + ), + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + + @Test + fun `SingleServantOnly - lineup1 (1B,2Q,3A,4B,5Q)`() { + val cards = AttackLineUps.SingleServantOnly.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> + assertThat(picked, braveChainEnum.toString()).isEmpty() + else -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.D, + CommandCard.Face.C, + CommandCard.Face.E, + ) + } + } + } + + @Test + fun `SingleServantOnly - lineup2 (1B,4B,5Q,2Q,3A)`() { + val cards = AttackLineUps.SingleServantOnly.lineup2 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> + assertThat(picked, braveChainEnum.toString()).isEmpty() + else -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.D, + CommandCard.Face.E, + CommandCard.Face.B, + CommandCard.Face.C, + ) + } + } + } + + @Test + fun `SingleServantOnly - lineup3 (5Q,2Q,3A,1B,4B)`() { + val cards = AttackLineUps.SingleServantOnly.lineup3 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> + assertThat(picked, braveChainEnum.toString()).isEmpty() + else -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.E, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.A, + CommandCard.Face.D, + ) + } + } + } + + // Scenario for when 2 card types are found but not the 3rd + @Test + fun `SingleServantOnly - lineup4 (1B,2Q,3Q,4B,5Q)`() { + val cards = AttackLineUps.SingleServantOnly.lineup4 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> + assertThat(picked, braveChainEnum.toString()).isEmpty() + else -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.E, + ) + } + } + } + + // Scenario for when 2 card types are found but not the 3rd + @Test + fun `SingleServantOnly - lineup5 (5Q,2Q,3Q,1B,4B)`() { + val cards = AttackLineUps.SingleServantOnly.lineup5 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> + assertThat(picked, braveChainEnum.toString()).isEmpty() + else -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.E, + CommandCard.Face.B, + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.D, + ) + } + } + } + + /** + * Two NP scenario + */ + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2 NP (1Kama, 3Nero), with npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.C), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick, + FieldSlot.C to CardTypeEnum.Arts + ) + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.B, + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.E, + ) + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2 NP (1Kama, 2Scathach), with npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick, + FieldSlot.B to CardTypeEnum.Quick + ) + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.E, + ) + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2 NP (1B, 2B), with npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Buster, + FieldSlot.B to CardTypeEnum.Buster + ) + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.B, + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.E, + ) + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 3 NP, with npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B, CommandCard.NP.C), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick, + FieldSlot.B to CardTypeEnum.Quick, + FieldSlot.C to CardTypeEnum.Arts, + ) + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 1 Unknown NP`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Unknown + ), + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + + @Test + fun `Unknown - lineup1 (1 Unknown)`() { + val cards = AttackLineUps.Unknown.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.E, + CommandCard.Face.D, + CommandCard.Face.A, + ) + } + } + + @Test + fun `Unknown - lineup1 (1 Unknown) + 1 Valid NP`() { + val cards = AttackLineUps.Unknown.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.E, + CommandCard.Face.A, + ) + } + } + + @Test + fun `Unknown - lineup1 (1 Unknown) + 1 Valid NP, with npTypes`() { + val cards = AttackLineUps.Unknown.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick + ), + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.E, + CommandCard.Face.A, + ) + } + } + + @Test + fun `Unknown - lineup1 (1 Unknown) + 1 Unknown NP`() { + val cards = AttackLineUps.Unknown.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Unknown + ), + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + + @Test + fun `Unknown - lineup2 (2 Unknown)`() { + val cards = AttackLineUps.Unknown.lineup2 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.C, + CommandCard.Face.E, + CommandCard.Face.D, + CommandCard.Face.A, + CommandCard.Face.B, + ) + } + } + + @Test + fun `Unknown - lineup3 (3 Unknown)`() { + val cards = AttackLineUps.Unknown.lineup3 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + + @Test + fun `Unknown - lineup3 (3 Unknown) + 1 Valid NP`() { + val cards = AttackLineUps.Unknown.lineup3 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.D, + CommandCard.Face.E, + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.C, + ) + } + } + + @Test + fun `Unknown - lineup3 (3 Unknown) + 1 Valid NP, with npTypes`() { + val cards = AttackLineUps.Unknown.lineup3 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick + ), + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.D, + CommandCard.Face.E, + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.C, + ) + } + } + + @Test + fun `Unknown - lineup4 (5 Unknown)`() { + val cards = AttackLineUps.Unknown.lineup4 + for (braveChainEnum in braveChainEnums) { + val picked = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } +} \ No newline at end of file diff --git a/scripts/src/test/java/io/github/fate_grand_automata/scripts/attack/BraveChainTest.kt b/scripts/src/test/java/io/github/fate_grand_automata/scripts/attack/BraveChainTest.kt new file mode 100644 index 000000000..413c14da4 --- /dev/null +++ b/scripts/src/test/java/io/github/fate_grand_automata/scripts/attack/BraveChainTest.kt @@ -0,0 +1,640 @@ +package io.github.fate_grand_automata.scripts.attack + +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.isEmpty +import assertk.assertions.isEqualTo +import io.github.fate_grand_automata.scripts.enums.BraveChainEnum +import io.github.fate_grand_automata.scripts.models.CommandCard +import io.github.fate_grand_automata.scripts.models.NPUsage +import io.github.fate_grand_automata.scripts.models.ParsedCard +import io.github.fate_grand_automata.scripts.modules.attack.AvoidChainHandler +import io.github.fate_grand_automata.scripts.modules.attack.BraveChainHandler +import kotlin.test.Test + +class BraveChainTest { + val braveChainEnums = BraveChainEnum.entries + + fun assertDefaultAvoidChain ( + cards: List, + picked: List, + braveChainEnum: BraveChainEnum, + npUsage: NPUsage = NPUsage.none, + ) { + val defaultAvoid = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = npUsage, + avoidCardChains = false, + )?.map { it.card } ?: emptyList() + assertThat(picked, BraveChainEnum.Avoid.toString()).isEqualTo(defaultAvoid) + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ)`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = BraveChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> assertDefaultAvoidChain( + cards = cards, + braveChainEnum = braveChainEnum, + picked = picked, + ) + else -> assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 1Kama-NP`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = BraveChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> assertDefaultAvoidChain( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + picked = picked, + ) + else -> assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2Scathach-NP`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = BraveChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> assertDefaultAvoidChain( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + picked = picked, + ) + BraveChainEnum.WithNP, + BraveChainEnum.Always, -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.E, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + ) + else -> assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 3Nero-NP`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = BraveChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> assertDefaultAvoidChain( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + picked = picked, + ) + BraveChainEnum.WithNP, + BraveChainEnum.Always, -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.E, + ) + else -> assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2 NP (1Kama + 2Scathach)`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = BraveChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B), 0), + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> assertDefaultAvoidChain( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B), 0), + picked = picked, + ) + else -> assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + } + + @Test + fun `Standard - lineup2 (1SB,5SQ,2KQ,3NA,4NA)`() { + val cards = AttackLineUps.Standard.lineup2 + for (braveChainEnum in braveChainEnums) { + val picked = BraveChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> assertDefaultAvoidChain( + cards = cards, + braveChainEnum = braveChainEnum, + picked = picked, + ) + else -> assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + } + + @Test + fun `Standard - lineup2 (1SB,5SQ,2KQ,3NA,4NA) + 1Kama-NP`() { + val cards = AttackLineUps.Standard.lineup2 + for (braveChainEnum in braveChainEnums) { + val picked = BraveChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> assertDefaultAvoidChain( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + picked = picked, + ) + else -> assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + } + + @Test + fun `Standard - lineup2 (1SB,5SQ,2KQ,3NA,4NA) + 2Scathach-NP`() { + val cards = AttackLineUps.Standard.lineup2 + for (braveChainEnum in braveChainEnums) { + val picked = BraveChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> assertDefaultAvoidChain( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + picked = picked, + ) + BraveChainEnum.WithNP, + BraveChainEnum.Always, -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.E, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + ) + else -> assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + } + + @Test + fun `Standard - lineup2 (1SB,5SQ,2KQ,3NA,4NA) + 3Nero-NP`() { + val cards = AttackLineUps.Standard.lineup2 + for (braveChainEnum in braveChainEnums) { + val picked = BraveChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> assertDefaultAvoidChain( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + picked = picked, + ) + BraveChainEnum.WithNP, + BraveChainEnum.Always, -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.A, + CommandCard.Face.E, + CommandCard.Face.B, + ) + else -> assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + } + + @Test + fun `BusterFocus - lineup01 (1KB,2KB,3NA,4NA,5KB)`() { + val cards = AttackLineUps.BusterFocus.lineup01 + for (braveChainEnum in braveChainEnums) { + val picked = BraveChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> assertDefaultAvoidChain( + cards = cards, + braveChainEnum = braveChainEnum, + picked = picked, + ) + BraveChainEnum.Always -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.E, + CommandCard.Face.C, + CommandCard.Face.D, + ) + else -> assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + } + + @Test + fun `BusterFocus - lineup01 (1KB,2KB,3NA,4NA,5KB) + 1 NP (2Kiyohime)`() { + val cards = AttackLineUps.BusterFocus.lineup01 + for (braveChainEnum in braveChainEnums) { + val picked = BraveChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> assertDefaultAvoidChain( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + picked = picked, + ) + BraveChainEnum.WithNP, + BraveChainEnum.Always, -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.E, + ) + else -> assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + } + + @Test + fun `BusterFocus - lineup01 (1KB,2KB,3NA,4NA,5KB) + 1 NP (3Nero)`() { + val cards = AttackLineUps.BusterFocus.lineup01 + for (braveChainEnum in braveChainEnums) { + val picked = BraveChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> assertDefaultAvoidChain( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + picked = picked, + ) + BraveChainEnum.WithNP, + BraveChainEnum.Always, -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.E, + ) + else -> assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + } + + @Test + fun `BusterFocus - lineup02 (1KB,5KB,2KB,3NA,4NA)`() { + val cards = AttackLineUps.BusterFocus.lineup02 + for (braveChainEnum in braveChainEnums) { + val picked = BraveChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> assertDefaultAvoidChain( + cards = cards, + braveChainEnum = braveChainEnum, + picked = picked, + ) + BraveChainEnum.Always -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.E, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + ) + else -> assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + } + + @Test + fun `BusterFocus - lineup05 (1KB,2KB,3NA,4NB,5KB)`() { + val cards = AttackLineUps.BusterFocus.lineup05 + for (braveChainEnum in braveChainEnums) { + val picked = BraveChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> assertDefaultAvoidChain( + cards = cards, + braveChainEnum = braveChainEnum, + picked = picked, + ) + BraveChainEnum.Always -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.E, + CommandCard.Face.C, + CommandCard.Face.D, + ) + else -> assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + } + + @Test + fun `Unknown - lineup1 (1 Unknown)`() { + val cards = AttackLineUps.Unknown.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = BraveChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> assertDefaultAvoidChain( + cards = cards, + braveChainEnum = braveChainEnum, + picked = picked, + ) + else -> assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + } + + @Test + fun `Unknown - lineup1 (1 Unknown) + 1 Valid NP`() { + val cards = AttackLineUps.Unknown.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = BraveChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> assertDefaultAvoidChain( + cards = cards, + braveChainEnum = braveChainEnum, + picked = picked, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + ) + BraveChainEnum.WithNP, + BraveChainEnum.Always, -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.B, + CommandCard.Face.E, + CommandCard.Face.A, + ) + else -> assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + } + + @Test + fun `Unknown - lineup2 (2 Unknown)`() { + val cards = AttackLineUps.Unknown.lineup2 + for (braveChainEnum in braveChainEnums) { + val picked = BraveChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> assertDefaultAvoidChain( + cards = cards, + braveChainEnum = braveChainEnum, + picked = picked, + ) + else -> assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + } + + @Test + fun `Unknown - lineup2 (2 Unknown) + 1 Valid NP`() { + val cards = AttackLineUps.Unknown.lineup2 + for (braveChainEnum in braveChainEnums) { + val picked = BraveChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> assertDefaultAvoidChain( + cards = cards, + braveChainEnum = braveChainEnum, + picked = picked, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + ) + BraveChainEnum.WithNP, + BraveChainEnum.Always, -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.E, + CommandCard.Face.A, + CommandCard.Face.B, + ) + else -> assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + } + + @Test + fun `Unknown - lineup3 (3 Unknown)`() { + val cards = AttackLineUps.Unknown.lineup3 + for (braveChainEnum in braveChainEnums) { + val picked = BraveChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> assertDefaultAvoidChain( + cards = cards, + braveChainEnum = braveChainEnum, + picked = picked, + ) + else -> assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + } + + @Test + fun `Unknown - lineup5 (1 Unknown, Brave Chain available)`() { + val cards = AttackLineUps.Unknown.lineup5 + for (braveChainEnum in braveChainEnums) { + val picked = BraveChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> assertDefaultAvoidChain( + cards = cards, + braveChainEnum = braveChainEnum, + picked = picked, + ) + BraveChainEnum.Always -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.E, + CommandCard.Face.D, + CommandCard.Face.C, + ) + else -> assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + } + + @Test + fun `Unknown - lineup5 (1 Unknown) + 1 Valid NP`() { + val cards = AttackLineUps.Unknown.lineup5 + for (braveChainEnum in braveChainEnums) { + val picked = BraveChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> assertDefaultAvoidChain( + cards = cards, + braveChainEnum = braveChainEnum, + picked = picked, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + ) + BraveChainEnum.WithNP, + BraveChainEnum.Always, -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.D, + CommandCard.Face.E, + CommandCard.Face.C, + ) + else -> assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + } + + @Test + fun `Unknown - lineup6 (2 Unknown, no Brave Chain available)`() { + val cards = AttackLineUps.Unknown.lineup6 + for (braveChainEnum in braveChainEnums) { + val picked = BraveChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> assertDefaultAvoidChain( + cards = cards, + braveChainEnum = braveChainEnum, + picked = picked, + ) + else -> assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + } + + @Test + fun `Unknown - lineup6 (2 Unknown) + 1 Valid NP`() { + val cards = AttackLineUps.Unknown.lineup6 + for (braveChainEnum in braveChainEnums) { + val picked = BraveChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> assertDefaultAvoidChain( + cards = cards, + braveChainEnum = braveChainEnum, + picked = picked, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + ) + BraveChainEnum.WithNP, + BraveChainEnum.Always, -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.B, + CommandCard.Face.E, + CommandCard.Face.D, + CommandCard.Face.A, + CommandCard.Face.C, + ) + else -> assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + } +} \ No newline at end of file diff --git a/scripts/src/test/java/io/github/fate_grand_automata/scripts/attack/CardChainPriorityIntegrationTest.kt b/scripts/src/test/java/io/github/fate_grand_automata/scripts/attack/CardChainPriorityIntegrationTest.kt new file mode 100644 index 000000000..e4845ab25 --- /dev/null +++ b/scripts/src/test/java/io/github/fate_grand_automata/scripts/attack/CardChainPriorityIntegrationTest.kt @@ -0,0 +1,795 @@ +package io.github.fate_grand_automata.scripts.attack + +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.isEqualTo +import io.github.fate_grand_automata.scripts.enums.BraveChainEnum +import io.github.fate_grand_automata.scripts.enums.CardTypeEnum +import io.github.fate_grand_automata.scripts.enums.ChainTypeEnum +import io.github.fate_grand_automata.scripts.models.CommandCard +import io.github.fate_grand_automata.scripts.models.FieldSlot +import io.github.fate_grand_automata.scripts.models.NPUsage +import io.github.fate_grand_automata.scripts.models.ParsedCard +import io.github.fate_grand_automata.scripts.modules.attack.MightyChainHandler +import io.github.fate_grand_automata.scripts.modules.attack.AttackPriorityHandler +import io.github.fate_grand_automata.scripts.modules.attack.AvoidChainHandler +import io.github.fate_grand_automata.scripts.modules.attack.BraveChainHandler +import io.github.fate_grand_automata.scripts.modules.attack.ColorChainHandler +import kotlin.test.Test + +class CardChainPriorityIntegrationTest { + fun getDefaultBraveChainResult ( + cards: List, + braveChainEnum: BraveChainEnum, + rearrange: Boolean = false, + npUsage: NPUsage = NPUsage.none, + ): List { + val results = BraveChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = npUsage, + ) ?: cards + return AttackPriorityHandler.rearrange( + cards = results, + rearrange = rearrange, + npUsage = npUsage, + ) + } + + fun getDefaultMightyChainResult ( + cards: List, + braveChainEnum: BraveChainEnum, + rearrange: Boolean = false, + npUsage: NPUsage = NPUsage.none, + npTypes: Map = emptyMap(), + ): List { + val results = MightyChainHandler.pick( + cards = cards, + npUsage = npUsage, + npTypes = npTypes, + braveChainEnum = braveChainEnum, + ) ?: cards + return AttackPriorityHandler.rearrange( + cards = results, + rearrange = rearrange, + npUsage = npUsage, + ) + } + + fun getDefaultColorChainResult ( + cardType: CardTypeEnum, + cards: List, + braveChainEnum: BraveChainEnum, + rearrange: Boolean = false, + npUsage: NPUsage = NPUsage.none, + npTypes: Map = emptyMap(), + ): List { + val results = ColorChainHandler.pick( + cardType = cardType, + cards = cards, + npUsage = npUsage, + npTypes = npTypes, + braveChainEnum = braveChainEnum, + ) ?: cards + return AttackPriorityHandler.rearrange( + cards = results, + rearrange = rearrange, + npUsage = npUsage, + ) + } + + fun getDefaultBusterChainResult ( + cards: List, + braveChainEnum: BraveChainEnum, + rearrange: Boolean = false, + npUsage: NPUsage = NPUsage.none, + npTypes: Map = emptyMap(), + ): List { + return getDefaultColorChainResult( + cardType = CardTypeEnum.Buster, + cards = cards, + npUsage = npUsage, + npTypes = npTypes, + braveChainEnum = braveChainEnum, + rearrange = rearrange, + ) + } + + fun getDefaultArtsChainResult ( + cards: List, + braveChainEnum: BraveChainEnum, + rearrange: Boolean = false, + npUsage: NPUsage = NPUsage.none, + npTypes: Map = emptyMap(), + ): List { + return getDefaultColorChainResult( + cardType = CardTypeEnum.Arts, + cards = cards, + npUsage = npUsage, + npTypes = npTypes, + braveChainEnum = braveChainEnum, + rearrange = rearrange, + ) + } + + fun getDefaultQuickChainResult ( + cards: List, + braveChainEnum: BraveChainEnum, + rearrange: Boolean = false, + npUsage: NPUsage = NPUsage.none, + npTypes: Map = emptyMap(), + ): List { + return getDefaultColorChainResult( + cardType = CardTypeEnum.Quick, + cards = cards, + npUsage = npUsage, + npTypes = npTypes, + braveChainEnum = braveChainEnum, + rearrange = rearrange, + ) + } + + fun getDefaultAvoidChainResult ( + cards: List, + braveChainEnum: BraveChainEnum, + rearrange: Boolean = false, + npUsage: NPUsage = NPUsage.none, + npTypes: Map = emptyMap(), + ): List { + val results = AvoidChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = npUsage, + npTypes = npTypes, + avoidCardChains = true, + ) ?: cards + return AttackPriorityHandler.rearrange( + cards = results, + rearrange = rearrange, + npUsage = npUsage, + ) + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ)`() { + val cards = AttackLineUps.Standard.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + ).map { it.card } + + // Expect SB,KQ,NA,NA,SQ - 12345 - ABCDE + assertThat(picked).containsExactly(CommandCard.Face.A, CommandCard.Face.B, CommandCard.Face.C, CommandCard.Face.D, CommandCard.Face.E) + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ), No Mighty allowed`() { + val cards = AttackLineUps.Standard.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + chainPriority = ChainTypeEnum.allPermitted.filter { it != ChainTypeEnum.Mighty }, + ).map { it.card } + val pickedDefault = getDefaultAvoidChainResult( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + ).map { it.card } + + assertThat(picked).isEqualTo(pickedDefault) + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ), No Mighty allowed, raw`() { + val cards = AttackLineUps.Standard.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + chainPriority = ChainTypeEnum.allPermitted.filter { it != ChainTypeEnum.Mighty }, + ).map { it.card } + + // Expect avoid SB,KQ,SQ,NA,NA - 12534 - ABECD + assertThat(picked).containsExactly(CommandCard.Face.A, CommandCard.Face.B, CommandCard.Face.E, CommandCard.Face.C, CommandCard.Face.D) + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ), No Mighty allowed, end before Avoid`() { + val cards = AttackLineUps.Standard.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + chainPriority = ChainTypeEnum.defaultOrder.filter { it != ChainTypeEnum.Mighty }, + ).map { it.card } + + assertThat(picked).isEqualTo(cards.map{ it.card }) + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ), with rearrange`() { + val cards = AttackLineUps.Standard.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + rearrange = true + ).map { it.card } + + // Same as above but swap position 2 and 3) + assertThat(picked).containsExactly(CommandCard.Face.A, CommandCard.Face.C, CommandCard.Face.B, CommandCard.Face.D, CommandCard.Face.E) + } + + @Test + fun `Standard - lineup2 (1SB,5SQ,2KQ,3NA,4NA)`() { + val cards = AttackLineUps.Standard.lineup2 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + ).map { it.card } + + // Expect 1SB,2KQ,3NA,5SQ,4NA - 15324 - AECBD + assertThat(picked).containsExactly(CommandCard.Face.A, CommandCard.Face.E, CommandCard.Face.C, CommandCard.Face.B, CommandCard.Face.D) + } + + @Test + fun `Standard - lineup2 (1SB,5SQ,2KQ,3NA,4NA), with rearrange`() { + val cards = AttackLineUps.Standard.lineup2 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + rearrange = true + ).map { it.card } + + // Same as above but swap position 2 and 3) + assertThat(picked).containsExactly(CommandCard.Face.A, CommandCard.Face.C, CommandCard.Face.E, CommandCard.Face.B, CommandCard.Face.D) + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 1Kama-NP`() { + val cards = AttackLineUps.Standard.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0) + ).map { it.card } + val pickedDefault = getDefaultBraveChainResult( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0) + ).map { it.card } + + // Unable to Brave Chain with Kama. Will ignore and return result of withNp + assertThat(picked).isEqualTo(pickedDefault) + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 1Kama-NP, with rearrange`() { + val cards = AttackLineUps.Standard.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + rearrange = true + ).map { it.card } + val pickedDefault = getDefaultBraveChainResult( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + rearrange = true + ).map { it.card } + + // Same as above but swap position 1 and 2 because of NP + assertThat(picked).isEqualTo(pickedDefault) + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 1Kama-NP, with npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick + ) + ).map { it.card } + + // Expect 1SB,3NA,2KQ,4NA,5SQ - 13245 - ACBDE + assertThat(picked).containsExactly(CommandCard.Face.A, CommandCard.Face.C, CommandCard.Face.B, CommandCard.Face.D, CommandCard.Face.E) + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 1Kama-NP, with rearrange & npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick + ), + rearrange = true + ).map { it.card } + + // Same as above but swap position 1 and 2 because of NP + assertThat(picked).containsExactly(CommandCard.Face.C, CommandCard.Face.A, CommandCard.Face.B, CommandCard.Face.D, CommandCard.Face.E) + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2Scathach-NP`() { + val cards = AttackLineUps.Standard.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + ).map { it.card } + val pickedDefault = getDefaultBraveChainResult( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + ).map { it.card } + + // No mighty chain. Expect NP Brave Chain output + assertThat(picked).isEqualTo(pickedDefault) + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2Scathach-NP, with rearrange`() { + val cards = AttackLineUps.Standard.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + rearrange = true + ).map { it.card } + val pickedDefault = getDefaultBraveChainResult( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + rearrange = true + ).map { it.card } + + assertThat(picked).isEqualTo(pickedDefault) + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2Scathach-NP, with npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.B to CardTypeEnum.Quick + ) + ).map { it.card } + val pickedDefault = getDefaultBraveChainResult( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + ).map { it.card } + + // No mighty chain. Expect NP Brave Chain output + assertThat(picked).isEqualTo(pickedDefault) + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2Scathach-NP, with npTypes, raw`() { + val cards = AttackLineUps.Standard.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.B to CardTypeEnum.Quick + ) + ).map { it.card } + + // No mighty chain. Expect NP Brave Chain output + // Expect 1SB,5SQ,2KQ,3NA,4NA - 15234 - AEBCD + assertThat(picked).containsExactly(CommandCard.Face.A, CommandCard.Face.E, CommandCard.Face.B, CommandCard.Face.C, CommandCard.Face.D) + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2Scathach-NP, with npTypes & rearrange`() { + val cards = AttackLineUps.Standard.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.B to CardTypeEnum.Quick + ), + rearrange = true + ).map { it.card } + val pickedDefault = getDefaultBraveChainResult( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + rearrange = true + ).map { it.card } + + // Same as above but swap position 2 and 3) + assertThat(picked).isEqualTo(pickedDefault) + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2Scathach-NP, with npTypes & rearrange, raw`() { + val cards = AttackLineUps.Standard.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.B to CardTypeEnum.Quick + ), + rearrange = true + ).map { it.card } + + // Same as above but swap position 2 and 3) + assertThat(picked).containsExactly(CommandCard.Face.E, CommandCard.Face.A, CommandCard.Face.B, CommandCard.Face.C, CommandCard.Face.D) + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 3Nero-NP`() { + val cards = AttackLineUps.Standard.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0) + ).map { it.card } + + // Attempt to Brave chain with Nero NP + // Expect 3NA,4NA,1SB,2KQ,5SQ - 34125 - CDABE + assertThat(picked).containsExactly(CommandCard.Face.C, CommandCard.Face.D, CommandCard.Face.A, CommandCard.Face.B, CommandCard.Face.E) + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 3Nero-NP, with rearranged`() { + val cards = AttackLineUps.Standard.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + rearrange = true + ).map { it.card } + + // Same as above but swap position 1 and 2 because of NP + assertThat(picked).containsExactly(CommandCard.Face.D, CommandCard.Face.C, CommandCard.Face.A, CommandCard.Face.B, CommandCard.Face.E) + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 3Nero-NP, with npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + npTypes = mapOf( + FieldSlot.C to CardTypeEnum.Arts + ) + ).map { it.card } + + // Expect 3NA,4NA,1SB,2KQ,5SQ - 34125 - CDABE + assertThat(picked).containsExactly(CommandCard.Face.C, CommandCard.Face.D, CommandCard.Face.A, CommandCard.Face.B, CommandCard.Face.E) + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 3Nero-NP, with npTypes using default chain`() { + val cards = AttackLineUps.Standard.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + npTypes = mapOf( + FieldSlot.C to CardTypeEnum.Arts + ), + ).map { it.card } + val pickedDefault = getDefaultBraveChainResult( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + ).map { it.card } + + assertThat(picked).isEqualTo(pickedDefault) + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 3Nero-NP, with rearrange & npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + npTypes = mapOf( + FieldSlot.C to CardTypeEnum.Arts + ), + rearrange = true + ).map { it.card } + + // Same as above but swap position 1 and 2 because of NP + assertThat(picked).containsExactly(CommandCard.Face.D, CommandCard.Face.C, CommandCard.Face.A, CommandCard.Face.B, CommandCard.Face.E) + } + + @Test + fun `SingleServantOnly - lineup1 (BQABQ)`() { + val cards = AttackLineUps.SingleServantOnly.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + ).map { it.card } + + // Expect same result as input + // Expect BQABQ / 12345 / ABCDE + assertThat(picked).containsExactly(CommandCard.Face.A, CommandCard.Face.B, CommandCard.Face.C, CommandCard.Face.D, CommandCard.Face.E) + } + + @Test + fun `SingleServantOnly - lineup1 (BQABQ), with rearranged`() { + val cards = AttackLineUps.SingleServantOnly.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + rearrange = true + ).map { it.card } + + // Same as above but swap position 2 and 3) + assertThat(picked).containsExactly(CommandCard.Face.A, CommandCard.Face.C, CommandCard.Face.B, CommandCard.Face.D, CommandCard.Face.E) + } + + @Test + fun `SingleServantOnly - lineup2 (BBQQA)`() { + val cards = AttackLineUps.SingleServantOnly.lineup2 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + ).map { it.card } + + // Expect 1SB,5SQ,3NA,4NA,2KQ / 15342 / AECDB + assertThat(picked).containsExactly(CommandCard.Face.A, CommandCard.Face.E, CommandCard.Face.C, CommandCard.Face.D, CommandCard.Face.B) + } + + @Test + fun `SingleServantOnly - lineup2 (BBQQA), with rearranged`() { + val cards = AttackLineUps.SingleServantOnly.lineup2 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + rearrange = true + ).map { it.card } + + // Same as above but swap position 2 and 3) + assertThat(picked).containsExactly(CommandCard.Face.A, CommandCard.Face.C, CommandCard.Face.E, CommandCard.Face.D, CommandCard.Face.B) + } + + @Test + fun `SingleServantOnly - lineup3 (5Q,2Q,3A,1B,4B)`() { + val cards = AttackLineUps.SingleServantOnly.lineup3 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + ).map { it.card } + + // Expect QABQB / 53124 / ECABD + assertThat(picked).containsExactly(CommandCard.Face.E, CommandCard.Face.C, CommandCard.Face.A, CommandCard.Face.B, CommandCard.Face.D) + } + + @Test + fun `SingleServantOnly - lineup3 (5Q,2Q,3A,1B,4B) - Buster priority`() { + val cards = AttackLineUps.SingleServantOnly.lineup3 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + chainPriority = listOf(ChainTypeEnum.Buster, ChainTypeEnum.Mighty), + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.B to CardTypeEnum.Buster + ), + ).map { it.card } + + assertThat(picked).containsExactly( + CommandCard.Face.A, + CommandCard.Face.D, + CommandCard.Face.E, + CommandCard.Face.B, + CommandCard.Face.C, + ) + } + + @Test + fun `SingleServantOnly - lineup3 (5Q,2Q,3A,1B,4B), with rearranged`() { + val cards = AttackLineUps.SingleServantOnly.lineup3 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + rearrange = true + ).map { it.card } + + // Same as above but swap position 2 and 3) + assertThat(picked).containsExactly(CommandCard.Face.E, CommandCard.Face.A, CommandCard.Face.C, CommandCard.Face.B, CommandCard.Face.D) + } + + // Scenario for when 2 card types are found but not the 3rd + @Test + fun `SingleServantOnly - lineup4 (1B,2Q,3Q,4B,5Q)`() { + val cards = AttackLineUps.SingleServantOnly.lineup4 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + ).map { it.card } + val pickedDefault = getDefaultQuickChainResult( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + ).map { it.card } + + assertThat(picked).isEqualTo(pickedDefault) + } + + // Scenario for when 2 card types are found but not the 3rd + @Test + fun `SingleServantOnly - lineup5 (5Q,2Q,3Q,1B,4B)`() { + val cards = AttackLineUps.SingleServantOnly.lineup5 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + ).map { it.card } + val pickedDefault = getDefaultQuickChainResult( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + ).map { it.card } + + assertThat(picked).isEqualTo(pickedDefault) + } + + /** + * Two NP scenario + */ + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2 NP (2Kama, 3Nero) - No mighty chain`() { + val cards = AttackLineUps.Standard.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.B, CommandCard.NP.C), 0) + ).map { it.card } + val pickedDefault = getDefaultBraveChainResult( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.B, CommandCard.NP.C), 0) + ).map { it.card } + + // Should fall back to default behaviour + assertThat(picked).isEqualTo(pickedDefault) + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2 NP (1Kama, 3Nero), with npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.C), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick, + FieldSlot.C to CardTypeEnum.Arts + ) + ).map { it.card } + + // Fall back to Mighty chain + val pickedDefault = getDefaultMightyChainResult( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick, + FieldSlot.B to CardTypeEnum.Quick + ) + ).map { it.card } + + // Expect SB,KQ,NA,NA,SQ - 12345 - ABCDE + assertThat(picked).containsExactly(CommandCard.Face.A, CommandCard.Face.B, CommandCard.Face.C, CommandCard.Face.D, CommandCard.Face.E) + // Default behaviour check + assertThat(picked).isEqualTo(pickedDefault) + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2 NP (1Kama, 2Scathach) - No mighty chain`() { + val cards = AttackLineUps.Standard.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B), 0), + ).map { it.card } + val pickedDefault = getDefaultBraveChainResult( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B), 0) + ).map { it.card } + + // Should fall back to default behaviour + assertThat(picked).isEqualTo(pickedDefault) + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2 NP (1Kama, 2Scathach), with npTypes - No mighty chain`() { + val cards = AttackLineUps.Standard.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick, + FieldSlot.B to CardTypeEnum.Quick + ) + ).map { it.card } + + // Fall back to Quick chain + val pickedDefault = getDefaultQuickChainResult( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick, + FieldSlot.B to CardTypeEnum.Quick + ) + ).map { it.card } + + // Should fall back to default behaviour + assertThat(picked).isEqualTo(pickedDefault) + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 3 NP, with npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B, CommandCard.NP.C), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick, + FieldSlot.B to CardTypeEnum.Quick, + FieldSlot.C to CardTypeEnum.Arts, + ) + ).map { it.card } + val pickedDefault = getDefaultBraveChainResult( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B, CommandCard.NP.C), 0) + ).map { it.card } + + // Should fall back to default behaviour + assertThat(picked).isEqualTo(pickedDefault) + } + + // Test case for issue found with BB Worm farming map + // Scenario: All cards are BB, all are weak against enemy. + // Expected to be a Mighty Chain of A/NP/B, but becomes B/NP/A instead. + @Test + fun `SingleServantOnly - lineup6 (3A,5A,1B,4B,2Q)`() { + val cards = AttackLineUps.SingleServantOnly.lineup6_special + val picked = AttackPriorityHandler.pick( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.A), 1), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick, + ) + ).map { it.card } + + // Uses Mighty Chain + val pickedDefault = getDefaultMightyChainResult( + cards = cards, + braveChainEnum = BraveChainEnum.WithNP, + npUsage = NPUsage(setOf(CommandCard.NP.A), 1), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick, + ) + ).map { it.card } + + assertThat(picked).isEqualTo(pickedDefault) + assertThat(picked).containsExactly( + CommandCard.Face.C, + CommandCard.Face.A, + CommandCard.Face.E, + CommandCard.Face.D, + CommandCard.Face.B, + ) + } +} \ No newline at end of file diff --git a/scripts/src/test/java/io/github/fate_grand_automata/scripts/attack/ColorChainTest.kt b/scripts/src/test/java/io/github/fate_grand_automata/scripts/attack/ColorChainTest.kt new file mode 100644 index 000000000..be459f0c7 --- /dev/null +++ b/scripts/src/test/java/io/github/fate_grand_automata/scripts/attack/ColorChainTest.kt @@ -0,0 +1,1107 @@ +package io.github.fate_grand_automata.scripts.attack + +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.isEmpty +import assertk.assertions.isNotNull +import assertk.assertions.isNull +import io.github.fate_grand_automata.scripts.enums.BraveChainEnum +import io.github.fate_grand_automata.scripts.enums.CardTypeEnum +import io.github.fate_grand_automata.scripts.models.CommandCard +import io.github.fate_grand_automata.scripts.models.FieldSlot +import io.github.fate_grand_automata.scripts.models.NPUsage +import io.github.fate_grand_automata.scripts.modules.attack.BraveChainHandler +import kotlin.test.Test +import io.github.fate_grand_automata.scripts.modules.attack.ColorChainHandler + +class ColorChainTest { + val cardTypeList = CardTypeEnum.entries + + val braveChainEnums = BraveChainEnum.entries + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ)`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + + @Test + fun `Standard - lineup2 (1SB,5SQ,2KQ,3NA,4NA)`() { + val cards = AttackLineUps.Standard.lineup2 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 1Kama-NP, with npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick + ), + )?.map { it.card } ?: emptyList() + + when (cardType) { + CardTypeEnum.Quick -> + assertThat(result, "$braveChainEnum, $cardType").containsExactly( + CommandCard.Face.B, + CommandCard.Face.E, + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.D, + ) + + else -> assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2Scathach-NP, with npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.B to CardTypeEnum.Quick + ), + )?.map { it.card } ?: emptyList() + + when (cardType to braveChainEnum) { + CardTypeEnum.Quick to BraveChainEnum.None, + CardTypeEnum.Quick to BraveChainEnum.WithNP, + CardTypeEnum.Quick to BraveChainEnum.Always, + CardTypeEnum.Quick to BraveChainEnum.Avoid, -> + assertThat(result, "$braveChainEnum, $cardType").containsExactly( + CommandCard.Face.B, + CommandCard.Face.E, + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.D, + ) + else -> assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2Scathach-NP, with npTypes + forceBraveChain`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.B to CardTypeEnum.Quick + ), + forceBraveChain = true, + )?.map { it.card } ?: emptyList() + + when (cardType to braveChainEnum) { + CardTypeEnum.Quick to BraveChainEnum.None, + CardTypeEnum.Quick to BraveChainEnum.Avoid, -> + assertThat(result, "$braveChainEnum, $cardType").containsExactly( + CommandCard.Face.B, + CommandCard.Face.E, + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.D, + ) + else -> assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 3Nero-NP, with npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + npTypes = mapOf( + FieldSlot.C to CardTypeEnum.Arts + ), + )?.map { it.card } ?: emptyList() + + when (cardType to braveChainEnum) { + CardTypeEnum.Arts to BraveChainEnum.None, + CardTypeEnum.Arts to BraveChainEnum.Always, + CardTypeEnum.Arts to BraveChainEnum.WithNP, -> + assertThat(result, cardType.toString()).containsExactly( + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.E, + ) + + else -> assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + } + + @Test + fun `SingleServantOnly - lineup1 (1B,2Q,3A,4B,5Q)`() { + val cards = AttackLineUps.SingleServantOnly.lineup1 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + + @Test + fun `SingleServantOnly - lineup2 (1B,4B,5Q,2Q,3A)`() { + val cards = AttackLineUps.SingleServantOnly.lineup2 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + + @Test + fun `SingleServantOnly - lineup3 (5Q,2Q,3A,1B,4B)`() { + val cards = AttackLineUps.SingleServantOnly.lineup3 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + + @Test + fun `SingleServantOnly - lineup4 (1B,2Q,3Q,4B,5Q)`() { + val cards = AttackLineUps.SingleServantOnly.lineup4 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + when (cardType) { + CardTypeEnum.Quick -> + assertThat(result, cardType.toString()).containsExactly( + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.E, + CommandCard.Face.A, + CommandCard.Face.D, + ) + + else -> assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + } + + // Scenario for when 2 card types are found but not the 3rd + @Test + fun `SingleServantOnly - lineup5 (5Q,2Q,3Q,1B,4B)`() { + val cards = AttackLineUps.SingleServantOnly.lineup5 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + when (cardType) { + CardTypeEnum.Quick -> + assertThat(result, cardType.toString()).containsExactly( + CommandCard.Face.E, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.A, + CommandCard.Face.D, + ) + + else -> assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + } + + /** + * Two NP scenario + */ + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2 NP (1Kama, 3Nero), with npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.C), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick, + FieldSlot.C to CardTypeEnum.Arts + ), + )?.map { it.card } ?: emptyList() + + assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2 NP (1Kama, 2Scathach), with npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick, + FieldSlot.B to CardTypeEnum.Quick, + ), + )?.map { it.card } ?: emptyList() + + when (cardType) { + CardTypeEnum.Quick -> + assertThat(result, cardType.toString()).containsExactly( + CommandCard.Face.B, + CommandCard.Face.E, + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.D, + ) + + else -> assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2 NP (1B, 2B), with npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Buster, + FieldSlot.B to CardTypeEnum.Buster, + ), + )?.map { it.card } ?: emptyList() + + when (cardType) { + CardTypeEnum.Buster -> + assertThat(result, cardType.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.E, + ) + + else -> assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 3 NP, with npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B, CommandCard.NP.C), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick, + FieldSlot.B to CardTypeEnum.Quick, + FieldSlot.C to CardTypeEnum.Arts, + ), + )?.map { it.card } ?: emptyList() + + assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + + @Test + fun `BusterFocus - lineup1 (1KB,2KB,3NA,4NA,5KB)`() { + val cards = AttackLineUps.BusterFocus.lineup01 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + when (cardType to braveChainEnum) { + CardTypeEnum.Buster to BraveChainEnum.None, + CardTypeEnum.Buster to BraveChainEnum.Always, + CardTypeEnum.Buster to BraveChainEnum.WithNP, -> + assertThat(result, cardType.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.E, + CommandCard.Face.C, + CommandCard.Face.D, + ) + + else -> assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + } + + @Test + fun `BusterFocus - lineup1 (1KB,2KB,3NA,4NA,5KB) + forceBraveChain`() { + val cards = AttackLineUps.BusterFocus.lineup01 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + forceBraveChain = true, + )?.map { it.card } ?: emptyList() + + when (cardType to braveChainEnum) { + CardTypeEnum.Buster to BraveChainEnum.None, + CardTypeEnum.Buster to BraveChainEnum.Always, + CardTypeEnum.Buster to BraveChainEnum.WithNP, -> + assertThat(result, cardType.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.E, + CommandCard.Face.C, + CommandCard.Face.D, + ) + + else -> assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + } + + @Test + fun `BusterFocus - lineup1 (1KB,2KB,3NA,4NA,5KB) + 1 NP (2Kiyohime), with npTypes`() { + val cards = AttackLineUps.BusterFocus.lineup01 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.B to CardTypeEnum.Buster, + ), + )?.map { it.card } ?: emptyList() + + when (cardType to braveChainEnum) { + CardTypeEnum.Buster to BraveChainEnum.None, + CardTypeEnum.Buster to BraveChainEnum.Always, + CardTypeEnum.Buster to BraveChainEnum.WithNP, -> + assertThat(result, "$braveChainEnum, $cardType").containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.E, + CommandCard.Face.C, + CommandCard.Face.D, + ) + + else -> assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + } + + @Test + fun `BusterFocus - lineup1 (1KB,2KB,3NA,4NA,5KB) + 1 NP (3Nero), with npTypes`() { + val cards = AttackLineUps.BusterFocus.lineup01 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + npTypes = mapOf( + FieldSlot.C to CardTypeEnum.Arts, + ), + )?.map { it.card } ?: emptyList() + + when (cardType to braveChainEnum) { + CardTypeEnum.Arts to BraveChainEnum.None, + CardTypeEnum.Arts to BraveChainEnum.Always, + CardTypeEnum.Arts to BraveChainEnum.WithNP, -> + assertThat(result, "$braveChainEnum, $cardType").containsExactly( + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.E, + ) + + else -> assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + } + + @Test + fun `BusterFocus - lineup05 (1KB,2KB,3NA,4NB,5KB)`() { + val cards = AttackLineUps.BusterFocus.lineup05 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + when (cardType to braveChainEnum) { + CardTypeEnum.Buster to BraveChainEnum.None, + CardTypeEnum.Buster to BraveChainEnum.WithNP, -> + assertThat(result, "$braveChainEnum, $cardType").containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.D, + CommandCard.Face.E, + CommandCard.Face.C, + ) + CardTypeEnum.Buster to BraveChainEnum.Always,-> + assertThat(result, "$braveChainEnum, $cardType").containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.E, + CommandCard.Face.C, + CommandCard.Face.D, + ) + CardTypeEnum.Buster to BraveChainEnum.Avoid, -> + assertThat(result, "$braveChainEnum, $cardType").containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.D, + CommandCard.Face.C, + CommandCard.Face.E, + ) + + else -> assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + } + + /** + * Unknown handling + */ + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 1 Unknown NP, with npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Unknown + ), + )?.map { it.card } ?: emptyList() + + assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + + @Test + fun `Unknown - lineup1 (1 Unknown)`() { + val cards = AttackLineUps.Unknown.lineup1 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + + @Test + fun `Unknown - lineup1 (1 Unknown) + 1Kama-NP, with npTypes`() { + val cards = AttackLineUps.Unknown.lineup1 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick + ), + )?.map { it.card } ?: emptyList() + + when (cardType) { + CardTypeEnum.Quick -> + assertThat(result, "$braveChainEnum, $cardType").containsExactly( + CommandCard.Face.B, + CommandCard.Face.E, + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.D, + ) + + else -> assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + } + + @Test + fun `Unknown - lineup1 (1 Unknown) + 3Nero-NP, with npTypes`() { + val cards = AttackLineUps.Unknown.lineup1 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + npTypes = mapOf( + FieldSlot.C to CardTypeEnum.Arts + ), + )?.map { it.card } ?: emptyList() + + when (cardType to braveChainEnum) { + CardTypeEnum.Arts to BraveChainEnum.None, + CardTypeEnum.Arts to BraveChainEnum.Always, + CardTypeEnum.Arts to BraveChainEnum.WithNP, -> + assertThat(result, "$braveChainEnum, $cardType").containsExactly( + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.E, + ) + + else -> assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + } + + @Test + fun `Unknown - lineup2 (2 Unknown)`() { + val cards = AttackLineUps.Unknown.lineup2 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + + @Test + fun `Unknown - lineup2 (2 Unknown) + 1Kama-NP, with npTypes`() { + val cards = AttackLineUps.Unknown.lineup2 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick + ), + )?.map { it.card } ?: emptyList() + + assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + + @Test + fun `Unknown - lineup2 (2 Unknown) + 3Nero-NP, with npTypes`() { + val cards = AttackLineUps.Unknown.lineup2 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + npTypes = mapOf( + FieldSlot.C to CardTypeEnum.Arts + ), + )?.map { it.card } ?: emptyList() + + when (cardType to braveChainEnum) { + CardTypeEnum.Arts to BraveChainEnum.None, + CardTypeEnum.Arts to BraveChainEnum.Always, + CardTypeEnum.Arts to BraveChainEnum.WithNP, -> + assertThat(result, "$braveChainEnum, $cardType").containsExactly( + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.E, + ) + + else -> assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + } + + @Test + fun `Unknown - lineup4 (5 Unknown)`() { + val cards = AttackLineUps.Unknown.lineup4 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + + @Test + fun `Unknown - lineup4 (5 Unknown) + 2 Valid NP`() { + val cards = AttackLineUps.Unknown.lineup4 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick, + FieldSlot.B to CardTypeEnum.Quick + ) + )?.map { it.card } ?: emptyList() + + assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + + @Test + fun `Unknown - lineup4 (5 Unknown) + 3 Valid NP`() { + val cards = AttackLineUps.Unknown.lineup4 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B, CommandCard.NP.C), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick, + FieldSlot.B to CardTypeEnum.Quick, + FieldSlot.C to CardTypeEnum.Quick + ) + ) + + when (cardType) { + CardTypeEnum.Quick -> assertThat(result, cardType.toString()).isNotNull() + else -> assertThat(result, cardType.toString()).isNull() + } + } + } + } + + @Test + fun `Unknown - lineup5 (1 Unknown, Buster Chain)`() { + val cards = AttackLineUps.Unknown.lineup5 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + when (cardType to braveChainEnum) { + CardTypeEnum.Buster to BraveChainEnum.None, + CardTypeEnum.Buster to BraveChainEnum.Always, + CardTypeEnum.Buster to BraveChainEnum.WithNP, -> + assertThat(result, "$braveChainEnum, $cardType").containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.E, + CommandCard.Face.C, + CommandCard.Face.D, + ) + + else -> assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + } + + @Test + fun `Unknown - lineup5 (1 Unknown) + 2 Valid NP, Arts Chain`() { + val cards = AttackLineUps.Unknown.lineup5 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Arts, + FieldSlot.B to CardTypeEnum.Arts, + ) + )?.map { it.card } ?: emptyList() + + when (cardType) { + CardTypeEnum.Arts -> + assertThat(result, "$braveChainEnum, $cardType").containsExactly( + CommandCard.Face.D, + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.E, + ) + + else -> assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + } + + @Test + fun `Unknown - lineup6 (2 Unknown)`() { + val cards = AttackLineUps.Unknown.lineup6 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + + @Test + fun `Unknown - lineup6 (1 Unknown) + 1 Valid NP, Buster Chain`() { + val cards = AttackLineUps.Unknown.lineup6 + for (braveChainEnum in braveChainEnums) { + for (cardType in cardTypeList) { + val result = ColorChainHandler.pick( + cards = cards, + cardType = cardType, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Buster, + ) + )?.map { it.card } ?: emptyList() + + when (cardType to braveChainEnum) { + CardTypeEnum.Buster to BraveChainEnum.None, + CardTypeEnum.Buster to BraveChainEnum.Always, + CardTypeEnum.Buster to BraveChainEnum.WithNP, -> + assertThat(result, "$braveChainEnum, $cardType").containsExactly( + CommandCard.Face.B, + CommandCard.Face.E, + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.D, + ) + + else -> assertThat(result, "$braveChainEnum, $cardType").isEmpty() + } + } + } + } + + /** + * Special cases + */ + @Test + fun `getCardsForAvoidBraveChain, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ)`() { + val cards = AttackLineUps.Standard.lineup1 + for (cardType in cardTypeList) { + val result = ColorChainHandler.getCardsForAvoidBraveChain( + cards = cards, + selectedCards = cards.filter { it.type == cardType }, + )?.map { it.card } ?: emptyList() + + assertThat(result, cardType.toString()).isEmpty() + } + } + + @Test + fun `getCardsForAvoidBraveChain, Standard - lineup2 (1SB,5SQ,2KQ,3NA,4NA)`() { + val cards = AttackLineUps.Standard.lineup2 + for (cardType in cardTypeList) { + val result = ColorChainHandler.getCardsForAvoidBraveChain( + cards = cards, + selectedCards = cards.filter { it.type == cardType }, + )?.map { it.card } ?: emptyList() + + assertThat(result, cardType.toString()).isEmpty() + } + } + + @Test + fun `getCardsForAvoidBraveChain, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + Slot 1 NP`() { + val cards = AttackLineUps.Standard.lineup1 + for (cardType in cardTypeList) { + val result = ColorChainHandler.getCardsForAvoidBraveChain( + cards = cards, + selectedCards = cards.filter { it.type == cardType }, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + )?.map { it.card } ?: emptyList() + + when (cardType) { + CardTypeEnum.Quick -> + assertThat(result, cardType.toString()).containsExactly( + CommandCard.Face.B, + CommandCard.Face.E, + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.D, + ) + else -> assertThat(result, cardType.toString()).isEmpty() + } + } + } + + @Test + fun `getCardsForAvoidBraveChain, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + Slot 2 NP`() { + val cards = AttackLineUps.Standard.lineup1 + for (cardType in cardTypeList) { + val result = ColorChainHandler.getCardsForAvoidBraveChain( + cards = cards, + selectedCards = cards.filter { it.type == cardType }, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + )?.map { it.card } ?: emptyList() + + when (cardType) { + CardTypeEnum.Quick -> + assertThat(result, cardType.toString()).containsExactly( + CommandCard.Face.B, + CommandCard.Face.E, + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.D, + ) + else -> assertThat(result, cardType.toString()).isEmpty() + } + } + } + + @Test + fun `getCardsForAvoidBraveChain, Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + Slot 3 NP`() { + val cards = AttackLineUps.Standard.lineup1 + for (cardType in cardTypeList) { + val result = ColorChainHandler.getCardsForAvoidBraveChain( + cards = cards, + selectedCards = cards.filter { it.type == cardType }, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + )?.map { it.card } ?: emptyList() + + when (cardType) { + CardTypeEnum.Quick -> + assertThat(result, cardType.toString()).containsExactly( + CommandCard.Face.B, + CommandCard.Face.E, + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.D, + ) + else -> assertThat(result, cardType.toString()).isEmpty() + } + } + } + + @Test + fun `getCardsForAvoidBraveChain, Standard - lineup2 (1SB,5SQ,2KQ,3NA,4NA) + Slot 1 NP`() { + val cards = AttackLineUps.Standard.lineup2 + for (cardType in cardTypeList) { + val result = ColorChainHandler.getCardsForAvoidBraveChain( + cards = cards, + selectedCards = cards.filter { it.type == cardType }, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + )?.map { it.card } ?: emptyList() + + when (cardType) { + CardTypeEnum.Quick -> + assertThat(result, cardType.toString()).containsExactly( + CommandCard.Face.E, + CommandCard.Face.B, + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.D, + ) + else -> assertThat(result, cardType.toString()).isEmpty() + } + } + } + + @Test + fun `getCardsForAvoidBraveChain, Standard - lineup2 (1SB,5SQ,2KQ,3NA,4NA) + Slot 2 NP`() { + val cards = AttackLineUps.Standard.lineup2 + for (cardType in cardTypeList) { + val result = ColorChainHandler.getCardsForAvoidBraveChain( + cards = cards, + selectedCards = cards.filter { it.type == cardType }, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + )?.map { it.card } ?: emptyList() + + when (cardType) { + CardTypeEnum.Quick -> + assertThat(result, cardType.toString()).containsExactly( + CommandCard.Face.E, + CommandCard.Face.B, + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.D, + ) + else -> assertThat(result, cardType.toString()).isEmpty() + } + } + } + + @Test + fun `getCardsForAvoidBraveChain, Standard - lineup2 (1SB,5SQ,2KQ,3NA,4NA) + Slot 3 NP`() { + val cards = AttackLineUps.Standard.lineup2 + for (cardType in cardTypeList) { + val result = ColorChainHandler.getCardsForAvoidBraveChain( + cards = cards, + selectedCards = cards.filter { it.type == cardType }, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + )?.map { it.card } ?: emptyList() + + when (cardType) { + CardTypeEnum.Quick -> + assertThat(result, cardType.toString()).containsExactly( + CommandCard.Face.E, + CommandCard.Face.B, + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.D, + ) + else -> assertThat(result, cardType.toString()).isEmpty() + } + } + } + + @Test + fun `getCardsForAvoidBraveChain, BusterFocus - lineup01 (1KB,2KB,3NA,4NA,5KB)`() { + val cards = AttackLineUps.BusterFocus.lineup01 + for (cardType in cardTypeList) { + val result = ColorChainHandler.getCardsForAvoidBraveChain( + cards = cards, + selectedCards = cards.filter { it.type == cardType }, + )?.map { it.card } ?: emptyList() + + assertThat(result, cardType.toString()).isEmpty() + } + } + + @Test + fun `getCardsForAvoidBraveChain, BusterFocus - lineup02 (1KB,5KB,2KB,3NA,4NA)`() { + val cards = AttackLineUps.BusterFocus.lineup02 + for (cardType in cardTypeList) { + val result = ColorChainHandler.getCardsForAvoidBraveChain( + cards = cards, + selectedCards = cards.filter { it.type == cardType }, + )?.map { it.card } ?: emptyList() + + assertThat(result, cardType.toString()).isEmpty() + } + } +} \ No newline at end of file diff --git a/scripts/src/test/java/io/github/fate_grand_automata/scripts/attack/MightyChainTest.kt b/scripts/src/test/java/io/github/fate_grand_automata/scripts/attack/MightyChainTest.kt new file mode 100644 index 000000000..df434537f --- /dev/null +++ b/scripts/src/test/java/io/github/fate_grand_automata/scripts/attack/MightyChainTest.kt @@ -0,0 +1,997 @@ +package io.github.fate_grand_automata.scripts.attack + +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.isEmpty +import io.github.fate_grand_automata.scripts.enums.BraveChainEnum +import io.github.fate_grand_automata.scripts.enums.CardTypeEnum +import io.github.fate_grand_automata.scripts.models.CommandCard +import io.github.fate_grand_automata.scripts.models.FieldSlot +import io.github.fate_grand_automata.scripts.models.NPUsage +import io.github.fate_grand_automata.scripts.modules.attack.MightyChainHandler +import kotlin.test.Test + +class MightyChainTest { + val braveChainEnums = BraveChainEnum.entries + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ)`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.E, + ) + } + } + + @Test + fun `Standard - lineup2 (1SB,5SQ,2KQ,3NA,4NA)`() { + val cards = AttackLineUps.Standard.lineup2 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.E, + CommandCard.Face.C, + CommandCard.Face.B, + CommandCard.Face.D, + ) + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 1Kama-NP`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + )?.map { it.card } ?: emptyList() + + // Cannot detect NP type, so unable to Mighty Chain + assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 1Kama-NP, with npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick + ) + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.B, + CommandCard.Face.D, + CommandCard.Face.E, + ) + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2Scathach-NP - No mighty chain`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0) + )?.map { it.card } ?: emptyList() + + // Unable to Mighty Chain with Scathach NP since type is unknown. + assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2Scathach-NP, with npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.B to CardTypeEnum.Quick + ) + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.B, + CommandCard.Face.D, + CommandCard.Face.E, + ) + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2Scathach-NP, with npTypes + forceBraveChain`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.B to CardTypeEnum.Quick + ), + forceBraveChain = true, + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.WithNP, + BraveChainEnum.Always -> assertThat(picked, braveChainEnum.toString()).isEmpty() + else -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.B, + CommandCard.Face.D, + CommandCard.Face.E, + ) + } + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2Scathach-NP, with Arts as npType`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.B to CardTypeEnum.Arts + ) + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.WithNP, + BraveChainEnum.Always -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.E, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + ) + else -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.E, + ) + } + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 3Nero-NP - No mighty chain`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0) + )?.map { it.card } ?: emptyList() + + // Unable to Mighty Chain with Nero NP since type is unknown. + assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 3Nero-NP, with npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + npTypes = mapOf( + FieldSlot.C to CardTypeEnum.Arts + ), + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.E, + ) + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 3Nero-NP, with npTypes + forceBraveChain`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + npTypes = mapOf( + FieldSlot.C to CardTypeEnum.Arts + ), + forceBraveChain = true, + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.WithNP, + BraveChainEnum.Always -> assertThat(picked, braveChainEnum.toString()).isEmpty() + else -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.E, + ) + } + } + } + + @Test + fun `Standard - lineup2 (1SB,5SQ,2KQ,3NA,4NA) + 1Kama-NP, with npTypes`() { + val cards = AttackLineUps.Standard.lineup2 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick + ), + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.E, + CommandCard.Face.B, + CommandCard.Face.D, + ) + } + } + + @Test + fun `Standard - lineup2 (1SB,5SQ,2KQ,3NA,4NA) + 2Scathach-NP, with npTypes`() { + val cards = AttackLineUps.Standard.lineup2 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.B to CardTypeEnum.Quick + ), + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.E, + CommandCard.Face.B, + CommandCard.Face.D, + ) + } + } + + @Test + fun `Standard - lineup2 (1SB,5SQ,2KQ,3NA,4NA) + 2Scathach-NP, with npTypes + forceBraveChain`() { + val cards = AttackLineUps.Standard.lineup2 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.B to CardTypeEnum.Quick + ), + forceBraveChain = true, + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.WithNP, + BraveChainEnum.Always -> assertThat(picked, braveChainEnum.toString()).isEmpty() + else -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.E, + CommandCard.Face.B, + CommandCard.Face.D, + ) + } + } + } + + @Test + fun `Standard - lineup2 (1SB,5SQ,2KQ,3NA,4NA) + 2Scathach-NP, with Arts as npType`() { + val cards = AttackLineUps.Standard.lineup2 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.B to CardTypeEnum.Arts + ) + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.None, + BraveChainEnum.WithNP, + BraveChainEnum.Always -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.E, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + ) + else -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.E, + CommandCard.Face.C, + CommandCard.Face.D, + ) + } + } + } + + @Test + fun `Standard - lineup2 (1SB,5SQ,2KQ,3NA,4NA) + 3Nero-NP, with npTypes`() { + val cards = AttackLineUps.Standard.lineup2 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + npTypes = mapOf( + FieldSlot.C to CardTypeEnum.Arts + ), + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.E, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + ) + } + } + + @Test + fun `Standard - lineup2 (1SB,5SQ,2KQ,3NA,4NA) + 3Nero-NP, with npTypes + forceBraveChain`() { + val cards = AttackLineUps.Standard.lineup2 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + npTypes = mapOf( + FieldSlot.C to CardTypeEnum.Arts + ), + forceBraveChain = true, + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.WithNP, + BraveChainEnum.Always -> assertThat(picked, braveChainEnum.toString()).isEmpty() + else -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.E, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + ) + } + } + } + + @Test + fun `SingleServantOnly - lineup1 (1B,2Q,3A,4B,5Q) + 3Nero-NP, with npTypes`() { + val cards = AttackLineUps.SingleServantOnly.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + npTypes = mapOf( + FieldSlot.C to CardTypeEnum.Arts + ), + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.E, + ) + } + } + + @Test + fun `SingleServantOnly - lineup1 (1B,2Q,3A,4B,5Q) + Buster NP, with npTypes`() { + val cards = AttackLineUps.SingleServantOnly.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.C), 0), + npTypes = mapOf( + FieldSlot.C to CardTypeEnum.Buster + ), + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.A, + CommandCard.Face.D, + CommandCard.Face.E, + ) + } + } + + @Test + fun `SingleServantOnly - lineup2 (1B,4B,5Q,2Q,3A)`() { + val cards = AttackLineUps.SingleServantOnly.lineup2 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.E, + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.B, + ) + } + } + + @Test + fun `SingleServantOnly - lineup3 (5Q,2Q,3A,1B,4B)`() { + val cards = AttackLineUps.SingleServantOnly.lineup3 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.E, + CommandCard.Face.C, + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.D + ) + } + } + + // Scenario for when 2 card types are found but not the 3rd + @Test + fun `SingleServantOnly - lineup4 (1B,2Q,3Q,4B,5Q) - No mighty chain`() { + val cards = AttackLineUps.SingleServantOnly.lineup4 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + + // Scenario for when 2 card types are found but not the 3rd + @Test + fun `SingleServantOnly - lineup5 (1Q,2Q,3Q,4B,5B) - No mighty chain`() { + val cards = AttackLineUps.SingleServantOnly.lineup5 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + + /** + * Two NP scenario + */ + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2 NP (2Kama, 3Nero) - No mighty chain`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.B, CommandCard.NP.C), 0) + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2 NP (1Kama, 3Nero), with npTypes`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.C), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick, + FieldSlot.C to CardTypeEnum.Arts + ) + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.E, + ) + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2 NP (1Kama, 2Scathach) - No mighty chain`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B), 0), + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 2 NP (1Kama, 2Scathach), with npTypes - No mighty chain`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick, + FieldSlot.B to CardTypeEnum.Quick + ) + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + + @Test + fun `Standard - lineup1 (1SB,2KQ,3NA,4NA,5SQ) + 3 NP, with npTypes - No mighty chain`() { + val cards = AttackLineUps.Standard.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A, CommandCard.NP.B, CommandCard.NP.C), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick, + FieldSlot.B to CardTypeEnum.Quick, + FieldSlot.C to CardTypeEnum.Arts, + ) + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + + /** + * Unknown handling + */ + @Test + fun `Unknown - lineup1 (1 Unknown)`() { + val cards = AttackLineUps.Unknown.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + + @Test + fun `Unknown - lineup1 (1 Unknown) + 1 Valid Buster NP, with npTypes`() { + val cards = AttackLineUps.Unknown.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Buster + ) + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.E, + CommandCard.Face.A, + ) + } + } + + @Test + fun `Unknown - lineup2 (2 Unknown)`() { + val cards = AttackLineUps.Unknown.lineup2 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + + @Test + fun `Unknown - lineup2 (2 Unknown) + 1 Valid Buster NP, with npTypes`() { + val cards = AttackLineUps.Unknown.lineup2 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Buster + ) + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.C, + CommandCard.Face.E, + CommandCard.Face.D, + CommandCard.Face.A, + CommandCard.Face.B, + ) + } + } + + @Test + fun `Unknown - lineup3 (3 Unknown)`() { + val cards = AttackLineUps.Unknown.lineup3 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + + @Test + fun `Unknown - lineup3 (3 Unknown) + 1 Valid Buster NP, with npTypes`() { + val cards = AttackLineUps.Unknown.lineup3 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Buster + ) + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.D, + CommandCard.Face.E, + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.C, + ) + } + } + + @Test + fun `Unknown - lineup4 (5 Unknown)`() { + val cards = AttackLineUps.Unknown.lineup4 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + + @Test + fun `Unknown - lineup5 (1 Unknown)`() { + val cards = AttackLineUps.Unknown.lineup5 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + + @Test + fun `Unknown - lineup5 (1 Unknown) + 1 Valid Quick NP, with npTypes`() { + val cards = AttackLineUps.Unknown.lineup5 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick + ) + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.D, + CommandCard.Face.B, + CommandCard.Face.E, + CommandCard.Face.C, + ) + } + } + + @Test + fun `Unknown - lineup6 (1 Unknown)`() { + val cards = AttackLineUps.Unknown.lineup6 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).isEmpty() + } + } + + @Test + fun `Unknown - lineup6 (1 Unknown) + 1 Valid Quick NP, with npTypes`() { + val cards = AttackLineUps.Unknown.lineup6 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick + ) + )?.map { it.card } ?: emptyList() + + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.B, + CommandCard.Face.D, + CommandCard.Face.E, + CommandCard.Face.A, + CommandCard.Face.C, + ) + } + } + + /** + * Special edge cases + */ + @Test + fun `getMightyChainWithoutBraveChain, MightyOutlier - lineup1 (1SB,2SA,3NA,4NA,5SQ)`() { + val cards = AttackLineUps.MightyOutlier.lineup1 + val picked = MightyChainHandler.getMightyChainWithoutBraveChain( + cards = cards, + selectedCards = AttackLineUps.MightyOutlier.lineup1_DefaultMightyChain + )?.map { it.card } ?: emptyList() + + assertThat(picked).containsExactly( + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.E, + ) + } + + @Test + fun `getMightyChainWithoutBraveChain, MightyOutlier - lineup1 (1SB,2SA,3NA,4NA,5SQ) + ScathachNP`() { + val cards = AttackLineUps.MightyOutlier.lineup1 + val picked = MightyChainHandler.getMightyChainWithoutBraveChain( + cards = cards, + selectedCards = AttackLineUps.MightyOutlier.lineup1_DefaultMightyChain_WithQuickNP, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + )?.map { it.card } ?: emptyList() + + assertThat(picked).containsExactly( + CommandCard.Face.A, + CommandCard.Face.C, + ) + } + + @Test + fun `getMightyChainWithoutBraveChain, MightyOutlier - lineup1 (1SB,2SA,3NA,4NA,5SQ) + Non-Scathach Buster NP`() { + val cards = AttackLineUps.MightyOutlier.lineup1 + val picked = MightyChainHandler.getMightyChainWithoutBraveChain( + cards = cards, + selectedCards = AttackLineUps.MightyOutlier.lineup1_DefaultMightyChain_WithNonScathachBusterNP, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + )?.map { it.card } ?: emptyList() + + assertThat(picked).containsExactly( + CommandCard.Face.B, + CommandCard.Face.E, + ) + } + + @Test + fun `getMightyChainWithoutBraveChain, MightyOutlier - lineup1 (1SB,2SA,3NA,4NA,5SQ) + Non-Scathach Arts NP`() { + val cards = AttackLineUps.MightyOutlier.lineup1 + val picked = MightyChainHandler.getMightyChainWithoutBraveChain( + cards = cards, + selectedCards = AttackLineUps.MightyOutlier.lineup1_DefaultMightyChain_WithNonScathachArtsNP, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + )?.map { it.card } ?: emptyList() + + assertThat(picked).containsExactly( + CommandCard.Face.A, + CommandCard.Face.E, + ) + } + + @Test + fun `getMightyChainWithoutBraveChain, MightyOutlier - lineup1 (1SB,2SA,3NA,4NA,5SQ) + Non-Scathach Quick NP`() { + val cards = AttackLineUps.MightyOutlier.lineup1 + val picked = MightyChainHandler.getMightyChainWithoutBraveChain( + cards = cards, + selectedCards = AttackLineUps.MightyOutlier.lineup1_DefaultMightyChain_WithQuickNP, + npUsage = NPUsage(setOf(CommandCard.NP.A), 0), + )?.map { it.card } ?: emptyList() + + assertThat(picked).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + ) + } + + @Test + fun `MightyOutlier - lineup1 (1SB,2SA,3NA,4NA,5SQ)`() { + val cards = AttackLineUps.MightyOutlier.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.E, + CommandCard.Face.B, + CommandCard.Face.D, + ) + else -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.E, + CommandCard.Face.C, + CommandCard.Face.D, + ) + } + } + } + + @Test + fun `MightyOutlier - lineup1 (1SB,2SA,3NA,4NA,5SQ) + 1 NP (Scathach)`() { + val cards = AttackLineUps.MightyOutlier.lineup1 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + npUsage = NPUsage(setOf(CommandCard.NP.B), 0), + npTypes = mapOf( + FieldSlot.A to CardTypeEnum.Quick, + ) + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.C, + CommandCard.Face.B, + CommandCard.Face.D, + CommandCard.Face.E, + ) + else -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + CommandCard.Face.E, + ) + } + } + } + + + @Test + fun `MightyOutlier - lineup2 (1SB,5SQ,2SA,3NA,4NA)`() { + val cards = AttackLineUps.MightyOutlier.lineup2 + for (braveChainEnum in braveChainEnums) { + val picked = MightyChainHandler.pick( + cards = cards, + braveChainEnum = braveChainEnum, + )?.map { it.card } ?: emptyList() + + when (braveChainEnum) { + BraveChainEnum.Avoid -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.E, + CommandCard.Face.C, + CommandCard.Face.B, + CommandCard.Face.D, + ) + else -> + assertThat(picked, braveChainEnum.toString()).containsExactly( + CommandCard.Face.A, + CommandCard.Face.E, + CommandCard.Face.B, + CommandCard.Face.C, + CommandCard.Face.D, + ) + } + } + } +} \ No newline at end of file diff --git a/wiki/img/attack/attack-priority-flowchart.png b/wiki/img/attack/attack-priority-flowchart.png new file mode 100644 index 000000000..653891385 Binary files /dev/null and b/wiki/img/attack/attack-priority-flowchart.png differ diff --git a/wiki/img/attack/avoid-chain-flowchart.png b/wiki/img/attack/avoid-chain-flowchart.png new file mode 100644 index 000000000..d14675c78 Binary files /dev/null and b/wiki/img/attack/avoid-chain-flowchart.png differ diff --git a/wiki/img/attack/brave-chain-flowchart.png b/wiki/img/attack/brave-chain-flowchart.png new file mode 100644 index 000000000..549d0e6bb Binary files /dev/null and b/wiki/img/attack/brave-chain-flowchart.png differ diff --git a/wiki/img/attack/brave-chain-menu.png b/wiki/img/attack/brave-chain-menu.png new file mode 100644 index 000000000..a9fe86660 Binary files /dev/null and b/wiki/img/attack/brave-chain-menu.png differ diff --git a/wiki/img/attack/brave-chain-options.png b/wiki/img/attack/brave-chain-options.png new file mode 100644 index 000000000..1e6caadfe Binary files /dev/null and b/wiki/img/attack/brave-chain-options.png differ diff --git a/wiki/img/attack/chain-priority-options-arts.png b/wiki/img/attack/chain-priority-options-arts.png new file mode 100644 index 000000000..203853ec3 Binary files /dev/null and b/wiki/img/attack/chain-priority-options-arts.png differ diff --git a/wiki/img/attack/chain-priority-options-avoid.png b/wiki/img/attack/chain-priority-options-avoid.png new file mode 100644 index 000000000..a5b5d8820 Binary files /dev/null and b/wiki/img/attack/chain-priority-options-avoid.png differ diff --git a/wiki/img/attack/chain-priority-options-buster.png b/wiki/img/attack/chain-priority-options-buster.png new file mode 100644 index 000000000..4a99fa314 Binary files /dev/null and b/wiki/img/attack/chain-priority-options-buster.png differ diff --git a/wiki/img/attack/chain-priority-options-default.png b/wiki/img/attack/chain-priority-options-default.png new file mode 100644 index 000000000..ee465b1e2 Binary files /dev/null and b/wiki/img/attack/chain-priority-options-default.png differ diff --git a/wiki/img/attack/chain-priority-options-none.png b/wiki/img/attack/chain-priority-options-none.png new file mode 100644 index 000000000..fdc3b6657 Binary files /dev/null and b/wiki/img/attack/chain-priority-options-none.png differ diff --git a/wiki/img/attack/chain-priority-toggle.png b/wiki/img/attack/chain-priority-toggle.png new file mode 100644 index 000000000..868b1dcc4 Binary files /dev/null and b/wiki/img/attack/chain-priority-toggle.png differ diff --git a/wiki/img/attack/color-chain-flowchart.png b/wiki/img/attack/color-chain-flowchart.png new file mode 100644 index 000000000..9ffed9883 Binary files /dev/null and b/wiki/img/attack/color-chain-flowchart.png differ diff --git a/wiki/img/attack/mighty-chain-flowchart.png b/wiki/img/attack/mighty-chain-flowchart.png new file mode 100644 index 000000000..c5d993a40 Binary files /dev/null and b/wiki/img/attack/mighty-chain-flowchart.png differ diff --git a/wiki/scripts/Battle.md b/wiki/scripts/Battle.md index 9d0e29cb6..9d62d59b0 100644 --- a/wiki/scripts/Battle.md +++ b/wiki/scripts/Battle.md @@ -38,6 +38,11 @@ - [Card Priority explanation](#card-priority-explanation) - [Symbols used in Card Priority](#symbols-used-in-card-priority) - [Servant Priority](#servant-priority) + - [Chain Priority](#chain-priority) + - [Interaction with other priorities](#interaction-with-other-priorities) + - [Mighty Chain](#mighty-chain) + - [Buster / Arts / Quick Chain](#buster--arts--quick-chain) + - [Avoid Chain](#avoid-chain) - [Rearrange Cards](#rearrange-cards) - [Brave Chain](#brave-chain) - [Support Selection](#support-selection) @@ -390,15 +395,15 @@ With cards in the high will have priority usage over cards on the low end #### Symbols used in Card Priority -- WB = Weak Buster -- WA = Weak Art -- WQ = Weak Quick -- B - Buster -- A - Art -- Q - Quick -- RB = Resist Buster -- RA = Resist Art -- RQ = Resist Quick +- $${\color{red}WB = Weak Buster}$$ +- $${\color{blue}WA = Weak Art}$$ +- $${\color{green}WQ = Weak Quick}$$ +- $${\color{red}B - Buster}$$ +- $${\color{blue}A - Art}$$ +- $${\color{green}Q - Quick}$$ +- $${\color{red}RB = Resist Buster}$$ +- $${\color{blue}RA = Resist Art}$$ +- $${\color{green}RQ = Resist Quick}$$ Cards can be changed around for different use case eg: @@ -437,13 +442,117 @@ Then you can now select the servant you want to prioritize. Follow the positioning on the [Party Mapping](#party-mapping) +___ + +### Chain Priority + +![Chain priority on](../img/attack/chain-priority-toggle.png) + +Chain Priority is a new feature that allows for attempts at `Mighty`, $${\color{red}Buster}$$, $${\color{blue}Arts}$$ or $${\color{green}Quick}$$ Chains, based on the order that the chains are chosen in. There is an additional option called "Avoid", which when used attempts to avoid any of the other chains. + +| Sample orders | Name | Description | +|:------------------------------------------------------------------------------------|:--------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ![Chain priority options default](../img/attack/chain-priority-options-default.png) | Default order | The default order used when Chain Priority is turned on. This disables Avoid Chain. | +| ![Chain priority options buster](../img/attack/chain-priority-options-buster.png) | $${\color{red}Buster}$$ focused | This order disables all chains except $${\color{red}Buster}$$ (highest priority) and `Mighty` (second priority). Failure to execute any of these chains will result in no other chain being (actively) used. | +| ![Chain priority options arts](../img/attack/chain-priority-options-arts.png) | $${\color{blue}Arts}$$ only | Only $${\color{blue}Arts}$$ chains will be attempted. All other chain checks are skipped. | +| ![Chain priority options avoid](../img/attack/chain-priority-options-avoid.png) | "Avoid" only | All chains will be avoided, unless it is absolutely impossible (e.g. all cards are $${\color{green}Quick}$$ cards). | +| ![Chain priority options none](../img/attack/chain-priority-options-none.png) | None | This completely skips all checks. It is almost the same as turning Chain Priority off, except that it uses Chain Priority's [Brave Chain](#brave-chain) handling instead of the default (which allows usage of the 'Always' option). | + +The way Chain Priority works is that it checks for Chains in the order that is shown (from left to right). If it finds a chain, then it will use that chain immediately. + +E.g. If Chain Priority is on and `Default order` is used, it will attempt to check for a `Mighty` Chain first. If there is no valid `Mighty` Chain, then it will attempt to check for a $${\color{red}Buster}$$ Chain. If there is no valid $${\color{red}Buster}$$ Chain, it will attempt to check for an $${\color{blue}Arts}$$ Chain and so on and so forth. If an $${\color{blue}Arts}$$ Chain was found, it will skip checking for a $${\color{green}Quick}$$ Chain using the cards and will immediately use the $${\color{blue}Arts}$$ Chain. However, if all options fail to find any usable Chain, it will fallback to Servant Priority + Card Priority to determine the cards used. + +| Flowchart of entire system | +|:--------------------------------------------------------------------------:| +| ![Attack Priority flow chart](../img/attack/attack-priority-flowchart.png) | + +#### Interaction with other priorities + +In general, the priority of card handling is in this order: + +``` +Brave Chains > Chain Priority > Servant Priority (if on) > Card Priority +``` + +This means that the [Brave Chain](#brave-chain) option is of the highest importance. This interaction with Chain Priority will be elaborated on in the corresponding sections. + +| Simplified Brave Chain flowchart / decision tree | +|:-----------------------------------------------------------------:| +| ![Brave Chain flowchart](../img/attack/brave-chain-flowchart.png) | + +Please refer to the [Brave Chain](#brave-chain) section for more information on what each Brave Chain option entails. + +#### Mighty Chain + +| Simplified Mighty Chain flowchart | +|:-------------------------------------------------------------------:| +| ![Mighty Chain flowchart](../img/attack/mighty-chain-flowchart.png) | + +Mighty Chains, by definition, require all 3 card types in a single chain to be executed. Mighty Chains can be done with multiple Servants or with a single Servant (thus forming a Brave Chain). The order of Mighty Chain cards is currently determined using the [Card Priority](#card-priority-optional). Thus, the default order is BAQ (Buster, Arts, Quick). + +
Advanced note + +> Since the default order is determined using Card Priority, it also takes into account Weak and Resist. Thus, if a weak card happens to be an Arts card and there is no weak Buster, since weak Arts is before a neutral Buster, the Mighty Chain will be ABQ. +> This slightly strange interaction is something to note when preparing the configuration, though it should rarely occur. + +
+ +| Brave Chains options | Interactions | +|:---------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Don't care | The default output of Mighty Chains will be used, regardless of whether a Brave Chain is used or not. A Mighty Chain will be used if there is one available, otherwise it will skip to the next Chain option. | +| With NP | Will always attempt to make a Mighty Brave Chain only when a single NP is being used.
If there is both a Brave Chain and a Mighty Chain separately available, the Brave Chain will be used over the Mighty Chain. | +| Always | Will always attempt to make a Mighty Brave Chain if one is available, regardless of the Servant.
If there is both a Brave Chain and a Mighty Chain separately available, the Brave Chain will be used over the Mighty Chain. | +| Avoid | Will actively avoid making Brave Chains. If the only Mighty Chain is a Mighty Brave Chain, it will be skipped. | + +#### Buster / Arts / Quick Chain + +| Simplified Buster / Arts / Quick Chain flowchart | +|:-----------------------------------------------------------------:| +| ![Color Chain flowchart](../img/attack/color-chain-flowchart.png) | + +A $${\color{red}Buster}$$ / $${\color{blue}Arts}$$ / $${\color{green}Quick}$$ Chain is made by choosing 3 cards of the same type. This is referred to a Color Chain for this system. + +| Brave Chains options | Interactions | +|:---------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Don't care | The default output of Color Chains will be used, regardless of whether a Brave Chain is used or not. A Color Chain will be used if there is one available, otherwise it will skip to the next Chain option. | +| With NP | Will always attempt to make a Color Brave Chain only when a single NP is being used.
If there is both a Brave Chain and a Color Chain separately available, the Brave Chain will be used over the Color Chain. | +| Always | Will always attempt to make a Color Brave Chain if one is available, regardless of the Servant.
If there is both a Brave Chain and a Color Chain separately available, the Brave Chain will be used over the Color Chain. | +| Avoid | Will actively avoid making Brave Chains. If the only Color Chain is a Color Brave Chain, it will be skipped. | + +#### Avoid Chain + +| Simplified Avoid Chain flowchart | +|:-----------------------------------------------------------------:| +| ![Avoid Chain flowchart](../img/attack/avoid-chain-flowchart.png) | + +The default behaviour of Avoid Chain in the system is to only avoid Mighty Chain and Color Chain as much as possible. This is the option, that, when combined with 'Avoid Brave Chains', tries its best to completely avoid any kind of chain possible. When it is not possible to avoid a Chain, it will be skipped over. + +The default implementation of this system attempts to stagger Servant cards if there are at least 2 Servants available that can make a non-Chain. This is due to the expected use case being to use this option only when face cards are to be used for efficient farming of simple and easy maps like Fuyuki. + +| Brave Chains options | Interactions | +|:---------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Don't care | The default only avoids Mighty and Color Chains. Thus, Brave Chains will occasionally occur with this option picked. | +| With NP | Attempts to make a Brave Chain (if one is available) only when a single NP is used, while avoiding Mighty and Color Chains.
| +| Always | Will attempt a Brave Chain while still avoiding a Mighty or Color Chain.
While the use-case of this option is near zero, it is still something that can be picked. | +| Avoid | Will actively avoid making Brave Chains. The most common use case. | + ___ ### Brave Chain -There are only 3 options for brace chains unless you use Servant Priority. +There are only 3 options for brave chains, unless [Chain Priority](#chain-priority) is on. + +![Battle Configs - Card Priority | Brave Chain menu](../img/attack/brave-chain-menu.png "Battle Configs - Card Priority | Brave Chain menu") + +![Battle Configs - Card Priority | Brave Chain options](../img/attack/brave-chain-options.png "Battle Configs - Card Priority | Brave Chain options") + +| Option | Description | +|:----------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Don't care | Will not attempt to form nor avoid any Brave Chains and will simply use whatever order other priorities have sorted in. Thus, there can be the occasional Brave Chains that occur. | +| With NP | Will always attempt to make a Brave Chain when a single NP is being used. If no NPs are used, Brave Chain checking is ignored. | +| Always (only if ([Chain Priority](#chain-priority) is on) | Will always attempt to make a Brave Chain when one is available, regardless of the Servant. | +| Avoid | Will actively avoid making Brave Chains if one is available, unless it is absolutely impossible (e.g. all cards belong to a single Servant) | -![Battle Configs - Card Priority | Brave Chain options](https://i.imgur.com/nhPPIM0.jpeg "Battle Configs - Card Priority | Brave Chain options") ___ ### Rearrange Cards @@ -456,7 +565,7 @@ Buster card is picked first, all face cards get a damage boost. The Rearrange Cards feature orders the 3 strongest cards so the 2nd strongest is used last. -![Face Cards](img/face-cards.jpg) +![Face Cards](../img/face-cards.jpg) If default card priority is used, FGA will pick these 3 cards when the Rearrange Cards option is enabled: - Jeanne Archer Buster (strongest card)