Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions forge-ai/src/main/java/forge/ai/ability/PumpAi.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa,
if (ph.getPhase().isAfter(PhaseType.COMBAT_FIRST_STRIKE_DAMAGE) || !ph.inCombat()) {
return false;
}
} else if (logic.equals("Weld")) {
// nothing is gained by welding early, so hold it to the last moment before our turn
return super.checkPhaseRestrictions(ai, sa, ph, "AtOppEOT");
}
return super.checkPhaseRestrictions(ai, sa, ph);
}
Expand Down Expand Up @@ -233,6 +236,8 @@ protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
} else if (aiLogic.startsWith("Donate")) {
// Donate step 1 - try to target an opponent, preferably one who does not have a donate target yet
return SpecialCardAi.Donate.considerTargetingOpponent(ai, sa);
} else if ("Weld".equals(aiLogic)) {
return doWeldLogic(ai, sa);
} else if (aiLogic.equals("InfernoOfTheStarMounts")) {
int numRedMana = ComputerUtilMana.determineLeftoverMana(new SpellAbility.EmptySa(source), ai, "R", false);
int currentPower = source.getNetPower();
Expand Down Expand Up @@ -345,6 +350,55 @@ protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}

/**
* Goblin Welder: the artifact targeted here is sacrificed and the artifact card targeted by the
* sub-ability replaces it, both belonging to the same player. Worth doing when that trade is an
* upgrade for us, or a downgrade for an opponent.
*/
private AiAbilityDecision doWeldLogic(final Player ai, final SpellAbility sa) {
final SpellAbility returnSa = sa.getSubAbility();
if (returnSa == null) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}

sa.resetTargets();
final CardCollection targetable = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa);

final List<Player> welders = Lists.newArrayList(ai);
welders.addAll(ai.getOpponents());
for (final Player p : welders) {
final boolean ours = p == ai;
final CardCollection board = CardLists.filter(targetable, CardPredicates.isController(p));
final Card sacrificed = ours ? weldSacrifice(ai, sa, board)
: ComputerUtilCard.getBestRemovalTargetAI(ai, board);
if (sacrificed == null) {
continue;
}

// once the parent target is set the sub-ability can choose what comes back, so what is
// weighed here is what will actually be targeted
sa.getTargets().add(sacrificed);
if (pumpMandatoryTarget(ai, returnSa)) {
final Card returned = returnSa.getTargetCard();
// upgrading ourselves, or leaving an opponent with the lesser artifact
if (ours ? returned.getCMC() > sacrificed.getCMC() : sacrificed.getCMC() > returned.getCMC()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
sa.resetTargets();
returnSa.resetTargets();
}

return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}

private static Card weldSacrifice(final Player ai, final SpellAbility sa, final CardCollection board) {
// anything already marked as expendable goes first, since mana value alone cannot tell a
// Sol Ring apart from a Chromatic Star
final Card expendable = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "SacCost", board, sa);
return expendable != null ? expendable : ComputerUtilCard.getWorstAI(board);
}

private boolean pumpTgtAI(final Player ai, final SpellAbility sa, final int defense, final int attack, final boolean mandatory,
boolean immediately) {
final List<String> keywords = sa.hasParam("KW") ? Arrays.asList(sa.getParam("KW").split(" & "))
Expand Down Expand Up @@ -419,6 +473,10 @@ private boolean pumpTgtAI(final Player ai, final SpellAbility sa, final int defe
return true;
}
return false;
} else if (sa.getParam("AILogic").equals("Weld")) {
// the parent has already settled whose artifact is going away, and the graveyard it
// has to come out of: best from our own, cheapest from theirs
return pumpMandatoryTarget(ai, sa);
} else if (sa.getParam("AILogic").equals("SameName")) {
return doSameNameLogic(ai, sa);
} else if (sa.getParam("AILogic").equals("SacOneEach")) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package forge.ai.ability;

import org.testng.annotations.Test;

import forge.ai.AITest;
import forge.game.Game;
import forge.game.card.Card;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.zone.ZoneType;

import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertTrue;

/**
* Goblin Welder targets the artifact that gets sacrificed and the graveyard card that replaces it,
* so the AI has to read both halves as one trade before it picks either one.
*/
public class GoblinWelderAiTest extends AITest {

private Card welder(Player ai) {
Card welder = addCard("Goblin Welder", ai);
welder.setSickness(false);
return welder;
}

private void endOfOpponentsTurn(Game game, Player opp) {
// Chromatic Star draws a card on the way out, and decking ends the game before we can look
for (Player p : game.getPlayers()) {
fillLibrary(p, 10);
}
game.getPhaseHandler().devModeSet(PhaseType.END_OF_TURN, opp);
game.getAction().checkStateEffects(true);
gameLoopUntilNextPhase(game);
}

@Test
public void weldsOurWorstArtifactIntoOurBest() {
Game game = initAndCreateGame();
Player ai = game.getPlayers().get(1);
Player opp = game.getPlayers().get(0);

welder(ai);
addCard("Chromatic Star", ai);
addCard("Sol Ring", ai);
addCardToZone("Wurmcoil Engine", ai, ZoneType.Graveyard);

endOfOpponentsTurn(game, opp);

assertEquals(1, countCardsWithName(game, "Wurmcoil Engine"));
assertEquals(1, countCardsWithName(game, "Chromatic Star", ZoneType.Graveyard));
assertEquals("kept the artifact worth keeping", 1, countCardsWithName(game, "Sol Ring"));
}

@Test
public void leavesTheOpponentNoBetterOffThanTheyWere() {
Game game = initAndCreateGame();
Player ai = game.getPlayers().get(1);
Player opp = game.getPlayers().get(0);

welder(ai);
addCard("Chromatic Star", ai);
addCardToZone("Ornithopter", ai, ZoneType.Graveyard);
// welding for the opponent here would trade their Wurmcoil up into a Blightsteel Colossus
addCard("Wurmcoil Engine", opp);
addCardToZone("Blightsteel Colossus", opp, ZoneType.Graveyard);

endOfOpponentsTurn(game, opp);

assertEquals(0, countCardsWithName(game, "Blightsteel Colossus"));
assertEquals(1, countCardsWithName(game, "Wurmcoil Engine"));
assertEquals("nothing of ours was spent either", 1, countCardsWithName(game, "Chromatic Star"));
}

@Test
public void tradesTheOpponentsArtifactDownForTheirOwnJunk() {
Game game = initAndCreateGame();
Player ai = game.getPlayers().get(1);
Player opp = game.getPlayers().get(0);

welder(ai);
addCard("Wurmcoil Engine", opp);
addCardToZone("Ornithopter", opp, ZoneType.Graveyard);

endOfOpponentsTurn(game, opp);

assertEquals(1, countCardsWithName(game, "Wurmcoil Engine", ZoneType.Graveyard));
assertTrue("the Ornithopter came back instead", countCardsWithName(game, "Ornithopter") == 1);
}

@Test
public void declinesASwapWorthNothing() {
Game game = initAndCreateGame();
Player ai = game.getPlayers().get(1);
Player opp = game.getPlayers().get(0);

welder(ai);
addCard("Sol Ring", ai);
// same mana value, so the AI cannot tell these apart and has nothing to gain by swapping
addCardToZone("Chromatic Star", ai, ZoneType.Graveyard);

endOfOpponentsTurn(game, opp);

assertEquals(1, countCardsWithName(game, "Sol Ring"));
assertEquals(0, countCardsWithName(game, "Chromatic Star"));
}

@Test
public void leavesItAloneWithNothingToGain() {
Game game = initAndCreateGame();
Player ai = game.getPlayers().get(1);
Player opp = game.getPlayers().get(0);

welder(ai);
addCard("Sol Ring", ai);
addCardToZone("Ornithopter", ai, ZoneType.Graveyard);

endOfOpponentsTurn(game, opp);

assertEquals(1, countCardsWithName(game, "Sol Ring"));
assertEquals(0, countCardsWithName(game, "Ornithopter"));
}
}
5 changes: 2 additions & 3 deletions forge-gui/res/cardsfolder/g/goblin_welder.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ Name:Goblin Welder
ManaCost:R
Types:Creature Goblin Artificer
PT:1/1
A:AB$ Pump | Cost$ T | ValidTgts$ Artifact | TgtPrompt$ Select target artifact a player controls | RememberObjects$ ThisTargetedCard | SubAbility$ DBTargetYard | StackDescription$ If both targets are still legal as this ability resolves, {p:TargetedController} simultaneously sacrifices {c:ThisTargetedCard} | SpellDescription$ Choose target artifact a player controls and target artifact card in that player's graveyard. If both targets are still legal as this ability resolves, that player simultaneously sacrifices the artifact and returns the artifact card to the battlefield.
SVar:DBTargetYard:DB$ Pump | ValidTgts$ Artifact | TargetsWithDefinedController$ ParentTargetedController | TgtPrompt$ Select target artifact card in that player's graveyard | TgtZone$ Graveyard | PumpZone$ Graveyard | ImprintCards$ ThisTargetedCard | StackDescription$ and returns {c:ThisTargetedCard} to the battlefield. | SubAbility$ DBBranch
A:AB$ Pump | Cost$ T | ValidTgts$ Artifact | AILogic$ Weld | TgtPrompt$ Select target artifact a player controls | RememberObjects$ ThisTargetedCard | SubAbility$ DBTargetYard | StackDescription$ If both targets are still legal as this ability resolves, {p:TargetedController} simultaneously sacrifices {c:ThisTargetedCard} | SpellDescription$ Choose target artifact a player controls and target artifact card in that player's graveyard. If both targets are still legal as this ability resolves, that player simultaneously sacrifices the artifact and returns the artifact card to the battlefield.
SVar:DBTargetYard:DB$ Pump | ValidTgts$ Artifact | AILogic$ Weld | TargetsWithDefinedController$ ParentTargetedController | TgtPrompt$ Select target artifact card in that player's graveyard | TgtZone$ Graveyard | PumpZone$ Graveyard | ImprintCards$ ThisTargetedCard | StackDescription$ and returns {c:ThisTargetedCard} to the battlefield. | SubAbility$ DBBranch
SVar:DBBranch:DB$ Branch | BranchConditionSVar$ TargetCheck | BranchConditionSVarCompare$ GE2 | TrueSubAbility$ DBSacrifice | FalseSubAbility$ DBCleanup
SVar:DBSacrifice:DB$ SacrificeAll | ValidCards$ Card.IsRemembered | SubAbility$ DBReturn
SVar:DBReturn:DB$ ChangeZone | Defined$ Imprinted | Origin$ Graveyard | Destination$ Battlefield | SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True | ClearImprinted$ True
SVar:TargetCheck:SVar$CheckRemem/Plus.CheckImprint
SVar:CheckRemem:Remembered$Valid Artifact.sharesControllerWith Imprinted
SVar:CheckImprint:Imprinted$Valid Artifact.sharesControllerWith Remembered
AI:RemoveDeck:All
AI:RemoveDeck:Random
Oracle:{T}: Choose target artifact a player controls and target artifact card in that player's graveyard. If both targets are still legal as this ability resolves, that player simultaneously sacrifices the artifact and returns the artifact card to the battlefield.