AI: reworked blockers selections:

* fixed game freezes for no-possible block configurations like Menace (#13290);
* fixed computer cheating to ignore block requirements like Menace (now AI will choose all required blockers instead 1);
* improved computer logic for blockers selection (try to sacrifice a creature instead game loose, simple use cases only);
* added freeze protection for bad or unsupported attacker-block configuration;
* refactor: deleted outdated AI code;
This commit is contained in:
Oleg Agafonov 2025-02-04 01:14:49 +04:00
parent a99871988d
commit 92b7ed8efc
12 changed files with 495 additions and 202 deletions

View file

@ -52,7 +52,7 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps {
}
@Test
public void test_Block_1_small_attacker_vs_1_small_blocker() {
public void test_Block_1_small_attacker_vs_1_small_blocker_same() {
addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1
addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 1); // 1/1
@ -72,6 +72,54 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps {
assertGraveyardCount(playerB, "Arbor Elf", 1);
}
@Test
public void test_Block_1_small_attacker_vs_1_small_blocker_better() {
addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1
//addCard(Zone.BATTLEFIELD, playerB, "Elvish Archers"); // 2/1 first strike
addCard(Zone.BATTLEFIELD, playerB, "Dregscape Zombie", 1); // 2/1
attack(1, playerA, "Arbor Elf");
// ai must ignore block to keep better creature alive
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
checkBlockers("no blockers", 1, playerB, "");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertLife(playerA, 20);
assertLife(playerB, 20 - 1);
assertPermanentCount(playerA, "Arbor Elf", 1);
assertPermanentCount(playerB, "Dregscape Zombie", 1);
}
@Test
public void test_Block_1_small_attacker_vs_1_small_blocker_better_but_player_die() {
addCustomEffect_TargetDamage(playerA, 19);
addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1
addCard(Zone.BATTLEFIELD, playerB, "Dregscape Zombie", 1); // 2/1
// prepare 1 life
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "target damage 19", playerB);
attack(1, playerA, "Arbor Elf");
// ai must keep better blocker in normal case, but now it must protect from lose and sacrifice it
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
checkBlockers("x1 blocker", 1, playerB, "Dregscape Zombie");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertLife(playerA, 20);
assertLife(playerB, 1);
assertGraveyardCount(playerA, "Arbor Elf", 1);
assertGraveyardCount(playerB, "Dregscape Zombie", 1);
}
@Test
public void test_Block_1_big_attacker_vs_1_small_blocker() {
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2

View file

@ -571,7 +571,7 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
}
@Test
public void test_MustBeBlocked_nothing() {
public void test_MustBeBlocked_nothing_human() {
// Fear of Being Hunted must be blocked if able.
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
@ -587,12 +587,30 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
}
@Test
public void test_MustBeBlocked_1_blocker() {
public void test_MustBeBlocked_nothing_AI() {
// Fear of Being Hunted must be blocked if able.
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
attack(1, playerA, "Fear of Being Hunted");
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
checkBlockers("no blocker", 1, playerB, "");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20 - 4);
}
@Test
public void test_MustBeBlocked_1_blocker_human() {
// Fear of Being Hunted must be blocked if able.
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
//
addCard(Zone.BATTLEFIELD, playerB, "Alpha Myr", 1); // 2/1
// auto-choose blocker
attack(1, playerA, "Fear of Being Hunted");
checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
checkBlockers("forced x1 blocker", 1, playerB, "Alpha Myr");
@ -606,15 +624,36 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
}
@Test
public void test_MustBeBlocked_many_blockers_good() {
public void test_MustBeBlocked_1_blocker_AI() {
// Fear of Being Hunted must be blocked if able.
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
//
addCard(Zone.BATTLEFIELD, playerB, "Alpha Myr", 1); // 2/1
// auto-choose blocker with AI
attack(1, playerA, "Fear of Being Hunted");
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
checkBlockers("forced x1 blocker", 1, playerB, "Alpha Myr");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20);
assertGraveyardCount(playerA, "Fear of Being Hunted", 1);
}
@Test
public void test_MustBeBlocked_many_blockers_good_AI() {
// Fear of Being Hunted must be blocked if able.
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
//
addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 10); // 3/3
// TODO: human logic can't be tested (until isHuman replaced by ~isComputer), so current use case will
// take first available blocker
// ai must choose any bear
attack(1, playerA, "Fear of Being Hunted");
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
checkBlockers("x1 optimal blocker", 1, playerB, "Spectral Bears");
@ -627,15 +666,15 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
}
@Test
public void test_MustBeBlocked_many_blockers_bad() {
public void test_MustBeBlocked_many_blockers_bad_AI() {
// Fear of Being Hunted must be blocked if able.
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
//
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 10); // 1/1
// TODO: human logic can't be tested (until isHuman replaced by ~isComputer), so current use case will
// take first available blocker
// ai don't want but must choose any bad memnite
attack(1, playerA, "Fear of Being Hunted");
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
checkBlockers("x1 optimal blocker", 1, playerB, "Memnite");
@ -645,12 +684,11 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
assertLife(playerB, 20);
assertPermanentCount(playerA, "Fear of Being Hunted", 1);
assertGraveyardCount(playerB, "Memnite", 1); // x1 blocker die
}
@Test
@Ignore
// TODO: enable and duplicate for AI -- after implement choose blocker logic and isHuman replace by ~isComputer
public void test_MustBeBlocked_many_blockers_optimal() {
public void test_MustBeBlocked_many_blockers_optimal_AI() {
// Fear of Being Hunted must be blocked if able.
addCard(Zone.BATTLEFIELD, playerA, "Fear of Being Hunted"); // 4/2
//
@ -659,7 +697,9 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 1); // 3/3
addCard(Zone.BATTLEFIELD, playerB, "Deadbridge Goliath", 1); // 5/5
// ai must choose optimal creature to kill but survive
attack(1, playerA, "Fear of Being Hunted");
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
checkAttackers("x1 attacker", 1, playerA, "Fear of Being Hunted");
checkBlockers("x1 optimal blocker", 1, playerB, "Deadbridge Goliath");
@ -669,6 +709,7 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
assertLife(playerB, 20);
assertGraveyardCount(playerA, "Fear of Being Hunted", 1);
assertGraveyardCount(playerB, 0);
}
@Test
@ -697,7 +738,7 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
}
@Test
public void test_MustBlocking_full_blockers() {
public void test_MustBlocking_full_blockers_human() {
// All creatures able to block target creature this turn do so.
addCard(Zone.HAND, playerA, "Bloodscent"); // {3}{G}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); // {3}{G}
@ -725,7 +766,37 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
}
@Test
public void test_MustBlocking_many_blockers() {
public void test_MustBlocking_full_blockers_AI() {
// All creatures able to block target creature this turn do so.
addCard(Zone.HAND, playerA, "Bloodscent"); // {3}{G}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); // {3}{G}
//
// Menace
// Each creature you control with menace can't be blocked except by three or more creatures.
addCard(Zone.BATTLEFIELD, playerA, "Sonorous Howlbonder"); // 2/2
//
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 3); // 1/1
// prepare
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bloodscent", "Sonorous Howlbonder");
// ai must choose all blockers anyway
attack(1, playerA, "Sonorous Howlbonder");
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
setChoiceAmount(playerA, 1); // assign damage to 1 of 3 blocking memnites
checkAttackers("x1 attacker", 1, playerA, "Sonorous Howlbonder");
checkBlockers("x3 blockers", 1, playerB, "Memnite", "Memnite", "Memnite");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20);
assertGraveyardCount(playerA, "Sonorous Howlbonder", 1);
}
@Test
public void test_MustBlocking_many_blockers_human() {
// possible bug: AI's blockers auto-fix assign too many blockers (e.g. x10 instead x3 by required effect)
// All creatures able to block target creature this turn do so.
@ -754,6 +825,38 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
assertGraveyardCount(playerA, "Sonorous Howlbonder", 1);
}
@Test
public void test_MustBlocking_many_blockers_AI() {
// possible bug: AI's blockers auto-fix assign too many blockers (e.g. x10 instead x3 by required effect)
// All creatures able to block target creature this turn do so.
addCard(Zone.HAND, playerA, "Bloodscent"); // {3}{G}
addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); // {3}{G}
//
// Menace
// Each creature you control with menace can't be blocked except by three or more creatures.
addCard(Zone.BATTLEFIELD, playerA, "Sonorous Howlbonder"); // 2/2
//
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 5); // 1/1
// prepare
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bloodscent", "Sonorous Howlbonder");
// ai must choose all blockers
attack(1, playerA, "Sonorous Howlbonder");
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
setChoiceAmount(playerA, 1); // assign damage to 1 of 3 blocking memnites
checkAttackers("x1 attacker", 1, playerA, "Sonorous Howlbonder");
checkBlockers("all blockers", 1, playerB, "Memnite", "Memnite", "Memnite", "Memnite", "Memnite");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20);
assertGraveyardCount(playerA, "Sonorous Howlbonder", 1);
}
@Test
@Ignore
// TODO: need exception fix - java.lang.UnsupportedOperationException: Sonorous Howlbonder is blocked by 1 creature(s). It has to be blocked by 3 or more.
@ -814,6 +917,7 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
@Test
@Ignore // TODO: need improve of block configuration auto-fix (block by x2 instead x1)
// looks like it's impossible for auto-fix (it's can remove blocker, but not add)
public void test_MustBeBlockedWithMenace_all_blockers() {
// At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creatures
// power until end of turn. That creature must be blocked this combat if able.
@ -844,7 +948,8 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
@Test
@Ignore // TODO: need improve of block configuration auto-fix (block by x2 instead x1)
public void test_MustBeBlockedWithMenace_many_blockers() {
// looks like it's impossible for auto-fix (it's can remove blocker, but not add)
public void test_MustBeBlockedWithMenace_many_low_blockers_human() {
// At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creatures
// power until end of turn. That creature must be blocked this combat if able.
addCard(Zone.BATTLEFIELD, playerA, "Neyith of the Dire Hunt"); // 3/3
@ -872,6 +977,42 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
assertGraveyardCount(playerA, "Alley Strangler", 0);
}
@Test
@Ignore // TODO: blockWithGoodTrade2 must support additional restrictions
public void test_MustBeBlockedWithMenace_many_low_blockers_AI() {
// At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creatures
// power until end of turn. That creature must be blocked this combat if able.
addCard(Zone.BATTLEFIELD, playerA, "Neyith of the Dire Hunt"); // 3/3
addCard(Zone.BATTLEFIELD, playerA, "Forest", 3);
//
// Menace
addCard(Zone.BATTLEFIELD, playerA, "Alley Strangler", 1); // 2/3
//
addCard(Zone.BATTLEFIELD, playerB, "Memnite", 10); // 1/1
// If the target creature has menace, two creatures must block it if able.
// (2020-06-23)
// AI must be forced to choose min x2 low blockers (it's possible to fail here after AI logic improve someday)
addTarget(playerA, "Alley Strangler"); // boost target
setChoice(playerA, true); // boost target
attack(1, playerA, "Alley Strangler");
setChoiceAmount(playerA, 1); // assign damage to 1 of 2 blocking memnites
setChoiceAmount(playerA, 1); // assign damage to 2 of 2 blocking memnites
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
checkAttackers("x1 attacker", 1, playerA, "Alley Strangler");
checkBlockers("x2 blockers", 1, playerB, "Memnite", "Memnite");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertLife(playerB, 20);
assertGraveyardCount(playerA, "Alley Strangler", 0);
assertGraveyardCount(playerB, "Memnite", 2); // x2 blockers must die
}
@Test
public void test_MustBeBlockedWithMenace_low_blockers_auto() {
// At the beginning of combat on your turn, you may pay {2}{R/G}. If you do, double target creatures
@ -985,7 +1126,6 @@ public class AttackBlockRestrictionsTest extends CardTestPlayerBaseWithAIHelps {
}
@Test
@Ignore // TODO: need to fix
public void test_MustBeBlockedWithMenace_low_big_blockers_AI() {
// bug: #13290, AI can try to use bigger creature to block

View file

@ -1996,7 +1996,7 @@ public class TestPlayer implements Player {
}
}
}
checkMultipleBlockers(game, blockedCreaturesList);
checkMultipleBlockers(game, blockedCreaturesList); // search wrong block commands
// AI FULL play if no actions available
if (!mustBlockByAction && (this.AIPlayer || this.AIRealGameSimulation)) {