mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-11 16:26:22 +00:00
GameAction: cleanup Static giving Abilities to the Stack (#8587)
* Add test that fails on master engine (before KeywordCollection sanity fix)
This commit is contained in:
@@ -5,6 +5,7 @@ import forge.ai.ComputerUtil;
|
|||||||
import forge.ai.PlayerControllerAi;
|
import forge.ai.PlayerControllerAi;
|
||||||
import forge.ai.simulation.GameStateEvaluator.Score;
|
import forge.ai.simulation.GameStateEvaluator.Score;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
|
import forge.game.GameActionUtil;
|
||||||
import forge.game.GameObject;
|
import forge.game.GameObject;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
@@ -131,6 +132,19 @@ public class GameSimulator {
|
|||||||
Card hostCard = (Card) copier.find(origHostCard);
|
Card hostCard = (Card) copier.find(origHostCard);
|
||||||
String desc = sa.getDescription();
|
String desc = sa.getDescription();
|
||||||
FCollectionView<SpellAbility> candidates = hostCard.getSpellAbilities();
|
FCollectionView<SpellAbility> candidates = hostCard.getSpellAbilities();
|
||||||
|
|
||||||
|
SpellAbility result = saMatcher(candidates, desc);
|
||||||
|
for (SpellAbility cSa : candidates) {
|
||||||
|
if (result != null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
result = saMatcher(GameActionUtil.getAlternativeCosts(cSa, aiPlayer, true), desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SpellAbility saMatcher(Iterable<SpellAbility> candidates, String desc) {
|
||||||
// first pass for accuracy (spells with alternative costs)
|
// first pass for accuracy (spells with alternative costs)
|
||||||
for (SpellAbility cSa : candidates) {
|
for (SpellAbility cSa : candidates) {
|
||||||
if (desc.equals(cSa.getDescription())) {
|
if (desc.equals(cSa.getDescription())) {
|
||||||
|
|||||||
@@ -223,6 +223,7 @@ public class GameAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cause != null && cause.isSpell() && c.equals(cause.getHostCard())) {
|
if (cause != null && cause.isSpell() && c.equals(cause.getHostCard())) {
|
||||||
|
copied.setCastFrom(zoneFrom);
|
||||||
copied.setCastSA(cause);
|
copied.setCastSA(cause);
|
||||||
copied.setSplitStateToPlayAbility(cause);
|
copied.setSplitStateToPlayAbility(cause);
|
||||||
|
|
||||||
@@ -232,6 +233,13 @@ public class GameAction {
|
|||||||
KeywordInterface kw = cause.getKeyword();
|
KeywordInterface kw = cause.getKeyword();
|
||||||
if (kw != null) {
|
if (kw != null) {
|
||||||
copied.addKeywordForStaticAbility(kw);
|
copied.addKeywordForStaticAbility(kw);
|
||||||
|
|
||||||
|
// CR 400.7g If an effect grants a nonland card an ability that allows it to be cast,
|
||||||
|
// that ability will continue to apply to the new object that card became after it moved to the stack as a result of being cast this way.
|
||||||
|
if (!cause.isIntrinsic()) {
|
||||||
|
kw.setHostCard(copied);
|
||||||
|
copied.addChangedCardKeywordsInternal(ImmutableList.of(kw), null, false, copied.getGameTimestamp(), kw.getStatic(), false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -300,7 +308,6 @@ public class GameAction {
|
|||||||
// Temporary disable commander replacement effect
|
// Temporary disable commander replacement effect
|
||||||
// 903.9a
|
// 903.9a
|
||||||
if (fromBattlefield && !toBattlefield && c.isCommander() && c.hasMergedCard()) {
|
if (fromBattlefield && !toBattlefield && c.isCommander() && c.hasMergedCard()) {
|
||||||
// Disable the commander replacement effect
|
|
||||||
c.getOwner().setCommanderReplacementSuppressed(true);
|
c.getOwner().setCommanderReplacementSuppressed(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,14 +452,8 @@ public class GameAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (zoneFrom.is(ZoneType.Stack) && toBattlefield) {
|
if (zoneFrom.is(ZoneType.Stack) && toBattlefield) {
|
||||||
// 400.7a Effects from static abilities that give a permanent spell on the stack an ability
|
// CR 400.7b Effects from static abilities that grant an ability to a permanent spell that functions on the battlefield
|
||||||
// that allows it to be cast for an alternative cost continue to apply to the permanent that spell becomes.
|
// continue to apply to the permanent that spell becomes
|
||||||
if (c.getCastSA() != null && !c.getCastSA().isIntrinsic() && c.getKeywords().contains(c.getCastSA().getKeyword())) {
|
|
||||||
KeywordInterface ki = c.getCastSA().getKeyword();
|
|
||||||
ki.setHostCard(copied);
|
|
||||||
copied.addChangedCardKeywordsInternal(ImmutableList.of(ki), null, false, copied.getGameTimestamp(), null, true);
|
|
||||||
}
|
|
||||||
// TODO hot fix for non-intrinsic offspring
|
|
||||||
Multimap<StaticAbility, KeywordInterface> addKw = MultimapBuilder.hashKeys().arrayListValues().build();
|
Multimap<StaticAbility, KeywordInterface> addKw = MultimapBuilder.hashKeys().arrayListValues().build();
|
||||||
for (KeywordInterface kw : c.getKeywords(Keyword.OFFSPRING)) {
|
for (KeywordInterface kw : c.getKeywords(Keyword.OFFSPRING)) {
|
||||||
if (!kw.isIntrinsic()) {
|
if (!kw.isIntrinsic()) {
|
||||||
@@ -465,7 +466,7 @@ public class GameAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 607.2q linked ability can find cards exiled as cost while it was a spell
|
// CR 607.2q linked ability can find cards exiled as cost while it was a spell
|
||||||
copied.addExiledCards(c.getExiledCards());
|
copied.addExiledCards(c.getExiledCards());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,7 +487,7 @@ public class GameAction {
|
|||||||
if (card.isRealCommander()) {
|
if (card.isRealCommander()) {
|
||||||
card.setMoveToCommandZone(true);
|
card.setMoveToCommandZone(true);
|
||||||
}
|
}
|
||||||
// 727.3e & 903.9a
|
// CR 727.3e & 903.9a
|
||||||
if (wasToken && !card.isRealToken() || card.isRealCommander()) {
|
if (wasToken && !card.isRealToken() || card.isRealCommander()) {
|
||||||
Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(card);
|
Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(card);
|
||||||
repParams.put(AbilityKey.CardLKI, card);
|
repParams.put(AbilityKey.CardLKI, card);
|
||||||
@@ -561,13 +562,6 @@ public class GameAction {
|
|||||||
// update static abilities after etb counters have been placed
|
// update static abilities after etb counters have been placed
|
||||||
checkStaticAbilities();
|
checkStaticAbilities();
|
||||||
|
|
||||||
// 400.7g try adding keyword back into card if it doesn't already have it
|
|
||||||
if (zoneTo.is(ZoneType.Stack) && cause != null && cause.isSpell() && !cause.isIntrinsic() && c.equals(cause.getHostCard())) {
|
|
||||||
if (cause.getKeyword() != null && !copied.getKeywords().contains(cause.getKeyword())) {
|
|
||||||
copied.addChangedCardKeywordsInternal(ImmutableList.of(cause.getKeyword()), null, false, game.getNextTimestamp(), null, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CR 603.6b
|
// CR 603.6b
|
||||||
if (toBattlefield) {
|
if (toBattlefield) {
|
||||||
zoneTo.saveLKI(copied, lastKnownInfo);
|
zoneTo.saveLKI(copied, lastKnownInfo);
|
||||||
@@ -827,19 +821,6 @@ public class GameAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (zoneTo.is(ZoneType.Stack)) {
|
|
||||||
c.setCastFrom(zoneFrom);
|
|
||||||
if (cause != null && cause.isSpell() && c.equals(cause.getHostCard())) {
|
|
||||||
c.setCastSA(cause);
|
|
||||||
} else {
|
|
||||||
c.setCastSA(null);
|
|
||||||
}
|
|
||||||
} else if (zoneFrom == null || !(zoneFrom.is(ZoneType.Stack) &&
|
|
||||||
(zoneTo.is(ZoneType.Battlefield) || zoneTo.is(ZoneType.Merged)))) {
|
|
||||||
c.setCastFrom(null);
|
|
||||||
c.setCastSA(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (c.isRealCommander()) {
|
if (c.isRealCommander()) {
|
||||||
c.setMoveToCommandZone(true);
|
c.setMoveToCommandZone(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2582,7 +2582,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|
|||||||
sbLong.append(" ").append(ManaCostParser.parse(k[1]));
|
sbLong.append(" ").append(ManaCostParser.parse(k[1]));
|
||||||
sbLong.append(" (").append(inst.getReminderText()).append(")");
|
sbLong.append(" (").append(inst.getReminderText()).append(")");
|
||||||
sbLong.append("\r\n");
|
sbLong.append("\r\n");
|
||||||
|
|
||||||
} else if (inst.getKeyword().equals(Keyword.COMPANION)) {
|
} else if (inst.getKeyword().equals(Keyword.COMPANION)) {
|
||||||
sbLong.append("Companion — ");
|
sbLong.append("Companion — ");
|
||||||
sbLong.append(((Companion)inst).getDescription());
|
sbLong.append(((Companion)inst).getDescription());
|
||||||
|
|||||||
@@ -151,19 +151,19 @@ public class AITest {
|
|||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
void playUntilStackClear(Game game) {
|
protected void playUntilStackClear(Game game) {
|
||||||
do {
|
do {
|
||||||
game.getPhaseHandler().mainLoopStep();
|
game.getPhaseHandler().mainLoopStep();
|
||||||
} while (!game.isGameOver() && !game.getStack().isEmpty());
|
} while (!game.isGameOver() && !game.getStack().isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
void playUntilPhase(Game game, PhaseType phase) {
|
protected void playUntilPhase(Game game, PhaseType phase) {
|
||||||
do {
|
do {
|
||||||
game.getPhaseHandler().mainLoopStep();
|
game.getPhaseHandler().mainLoopStep();
|
||||||
} while (!game.isGameOver() && !game.getPhaseHandler().is(phase));
|
} while (!game.isGameOver() && !game.getPhaseHandler().is(phase));
|
||||||
}
|
}
|
||||||
|
|
||||||
void playUntilNextTurn(Game game) {
|
protected void playUntilNextTurn(Game game) {
|
||||||
Player current = game.getPhaseHandler().getPlayerTurn();
|
Player current = game.getPhaseHandler().getPlayerTurn();
|
||||||
do {
|
do {
|
||||||
game.getPhaseHandler().mainLoopStep();
|
game.getPhaseHandler().mainLoopStep();
|
||||||
|
|||||||
@@ -2610,6 +2610,35 @@ public class GameSimulationTest extends SimulationTest {
|
|||||||
AssertJUnit.assertTrue(nonBasicForest.getType().hasSubtype("Mountain"));
|
AssertJUnit.assertTrue(nonBasicForest.getType().hasSubtype("Mountain"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHenzie() {
|
||||||
|
Game game = initAndCreateGame();
|
||||||
|
Player p = game.getPlayers().get(0);
|
||||||
|
game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
|
||||||
|
|
||||||
|
addCard("Henzie \"Toolbox\" Torre", p);
|
||||||
|
addCardToZone("Wastes", p, ZoneType.Library);
|
||||||
|
addCards("Plains", 5, p);
|
||||||
|
Card spell = addCardToZone("Serra Angel", p, ZoneType.Hand);
|
||||||
|
|
||||||
|
game.getAction().checkStaticAbilities();
|
||||||
|
List<SpellAbility> sas = spell.getAllPossibleAbilities(p, true);
|
||||||
|
SpellAbility blitz = sas.get(1);
|
||||||
|
|
||||||
|
GameSimulator sim = createSimulator(game, p);
|
||||||
|
game = sim.getSimulatedGameState();
|
||||||
|
sim.simulateSpellAbility(blitz);
|
||||||
|
spell = findCardWithName(game, "Serra Angel");
|
||||||
|
|
||||||
|
AssertJUnit.assertEquals(1, spell.getAmountOfKeyword(Keyword.BLITZ));
|
||||||
|
AssertJUnit.assertTrue(spell.hasKeyword(Keyword.HASTE));
|
||||||
|
|
||||||
|
playUntilNextTurn(game);
|
||||||
|
|
||||||
|
AssertJUnit.assertEquals(1, game.getPlayers().get(0).getCardsIn(ZoneType.Hand).size());
|
||||||
|
AssertJUnit.assertTrue(spell.isInZone(ZoneType.Graveyard));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test for "Volo's Journal" usage by the AI. This test checks if the AI correctly
|
* Test for "Volo's Journal" usage by the AI. This test checks if the AI correctly
|
||||||
* adds the correct types to the "Volo's Journal" when casting the spells in order
|
* adds the correct types to the "Volo's Journal" when casting the spells in order
|
||||||
|
|||||||
Reference in New Issue
Block a user