Merge branch 'miracle' into 'master'

Miracle is optional

Closes #2083

See merge request core-developers/forge!6201
This commit is contained in:
Michael Kamensky
2022-02-10 12:58:48 +00:00
9 changed files with 35 additions and 20 deletions

View File

@@ -153,10 +153,15 @@ public class AiAttackController {
}
}
/** Choose opponent for AI to attack here. Expand as necessary. */
/**
* Choose opponent for AI to attack here. Expand as necessary.
* No strategy to secure a second place instead, since Forge has no variant for that
*/
public static Player choosePreferredDefenderPlayer(Player ai) {
Player defender = ai.getWeakestOpponent(); //Concentrate on opponent within easy kill range
// TODO connect with evaluateBoardPosition and only fall back to random when no player is the biggest threat by a fair margin
if (defender.getLife() > 8) { //Otherwise choose a random opponent to ensure no ganging up on players
// TODO should we cache the random for each turn? some functions like shouldPumpCard base their decisions on the assumption who will be attacked
return ai.getOpponents().get(MyRandom.getRandom().nextInt(ai.getOpponents().size()));
@@ -720,7 +725,7 @@ public class AiAttackController {
continue;
}
boolean mustAttack = false;
// TODO for nextTurn check if it was temporary
// TODO this might result into attacking the wrong player
if (attacker.isGoaded()) {
mustAttack = true;
} else if (attacker.getSVar("MustAttack").equals("True")) {
@@ -737,7 +742,7 @@ public class AiAttackController {
mustAttack = true;
}
}
if (mustAttack || (attacker.getController().getMustAttackEntity() != null && nextTurn) || (attacker.getController().getMustAttackEntityThisTurn() != null && !nextTurn)) {
if (mustAttack ||attacker.getController().getMustAttackEntityThisTurn() != null) {
combat.addAttacker(attacker, defender);
attackersLeft.remove(attacker);
numForcedAttackers++;

View File

@@ -426,7 +426,6 @@ public class AiBlockController {
}
attackersLeft = new ArrayList<>(currentAttackers);
currentAttackers = new ArrayList<>(attackersLeft);
boolean considerTripleBlock = true;
@@ -437,6 +436,11 @@ public class AiBlockController {
continue;
}
// AI can't handle good blocks with more than three creatures yet
if (CombatUtil.getMinNumBlockersForAttacker(attacker, ai) > (considerTripleBlock ? 3 : 2)) {
continue;
}
int evalAttackerValue = ComputerUtilCard.evaluateCreature(attacker);
blockers = getPossibleBlockers(combat, attacker, blockersLeft, false);
@@ -446,11 +450,6 @@ public class AiBlockController {
int currentValue; // The value of the creatures in the blockgang
boolean foundDoubleBlock = false; // if true, a good double block is found
// AI can't handle good blocks with more than three creatures yet
if (CombatUtil.getMinNumBlockersForAttacker(attacker, ai) > (considerTripleBlock ? 3 : 2)) {
continue;
}
// Try to add blockers that could be destroyed, but are worth less than the attacker
// Don't use blockers without First Strike or Double Strike if attacker has it
usableBlockers = CardLists.filter(blockers, new Predicate<Card>() {
@@ -460,8 +459,7 @@ public class AiBlockController {
&& !ComputerUtilCombat.dealsFirstStrikeDamage(c, false, combat)) {
return false;
}
final boolean randomTrade = wouldLikeToRandomlyTrade(attacker, c, combat);
return lifeInDanger || randomTrade || ComputerUtilCard.evaluateCreature(c) + diff < ComputerUtilCard.evaluateCreature(attacker);
return lifeInDanger || wouldLikeToRandomlyTrade(attacker, c, combat) || ComputerUtilCard.evaluateCreature(c) + diff < ComputerUtilCard.evaluateCreature(attacker);
}
});
if (usableBlockers.size() < 2) {

View File

@@ -2822,8 +2822,6 @@ public class ComputerUtil {
pRating /= 5;
}
System.out.println("Board position evaluation for " + p + ": " + pRating);
if (pRating > bestBoardRating) {
bestBoardRating = pRating;
bestBoardPosition = p;

View File

@@ -830,7 +830,7 @@ public class ComputerUtilCombat {
}
// defender == null means unblocked
if ((defender == null) && mode == TriggerType.AttackerUnblocked) {
if (defender == null && mode == TriggerType.AttackerUnblocked) {
willTrigger = true;
if (!trigger.matchesValidParam("ValidCard", attacker)) {
return false;

View File

@@ -25,6 +25,9 @@ import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardFactoryUtil;
import forge.game.cost.Cost;
import forge.game.cost.CostDiscard;
import forge.game.cost.CostPart;
import forge.game.cost.CostReveal;
import forge.game.mana.ManaCostBeingPaid;
import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect;
@@ -70,7 +73,7 @@ public class PlayEffect extends SpellAbilityEffect {
Player controlledByPlayer = null;
long controlledByTimeStamp = -1;
final Game game = activator.getGame();
final boolean optional = sa.hasParam("Optional");
boolean optional = sa.hasParam("Optional");
boolean remember = sa.hasParam("RememberPlayed");
int amount = 1;
boolean hasTotalCMCLimit = sa.hasParam("WithTotalCMC");
@@ -330,7 +333,17 @@ public class PlayEffect extends SpellAbilityEffect {
}
if (!optional) {
tgtSA.getPayCosts().setMandatory(true);
// 118.8c
for (CostPart cost : tgtSA.getPayCosts().getCostParts()) {
if ((cost instanceof CostDiscard || cost instanceof CostReveal)
&& !cost.getType().equals("Card") && !cost.getType().equals("Random")) {
optional = true;
break;
}
}
if (!optional) {
tgtSA.getPayCosts().setMandatory(true);
}
}
if (sa.hasParam("PlayReduceCost")) {

View File

@@ -1452,7 +1452,7 @@ public class CardFactoryUtil {
final String manacost = k[1];
final String abStrReveal = "DB$ Reveal | Defined$ You | RevealDefined$ Self"
+ " | MiracleCost$ " + manacost;
final String abStrPlay = "DB$ Play | Defined$ Self | PlayCost$ " + manacost;
final String abStrPlay = "DB$ Play | Defined$ Self | Optional$ True | PlayCost$ " + manacost;
String revealed = "DB$ ImmediateTrigger | TriggerDescription$ CARDNAME - Miracle";

View File

@@ -6,6 +6,6 @@ K:Flying
T:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | CombatDamage$ True | Execute$ TrigDigUntil | TriggerZones$ Battlefield | TriggerDescription$ Whenever CARDNAME deals combat damage to a player, that player exiles cards from the top of their library until they exile an instant or sorcery card. You may cast that card without paying its mana cost. Then that player puts the exiled cards that weren't cast this way on the bottom of their library in a random order.
SVar:TrigDigUntil:DB$ DigUntil | Defined$ TriggeredTarget | Valid$ Instant,Sorcery | ValidDescription$ instant or sorcery | FoundDestination$ Exile | RevealedDestination$ Exile | RememberFound$ True | RememberRevealed$ True | IsCurse$ True | SubAbility$ DBPlay | SpellDescription$ Whenever CARDNAME deals combat damage to a player, that player exiles cards from the top of their library until they exile an instant or sorcery card. You may cast that card without paying its mana cost. Then that player puts the exiled cards that weren't cast this way on the bottom of their library in a random order.
SVar:DBPlay:DB$ Play | Defined$ Remembered | ValidZone$ Exile | Valid$ Instant.IsRemembered,Sorcery.IsRemembered | ValidSA$ Spell | WithoutManaCost$ True | RememberObjects$ Remembered | Optional$ True | ForgetTargetRemembered$ True | SubAbility$ DBRestRandomOrder
SVar:DBRestRandomOrder:DB$ ChangeZoneAll | ChangeType$ Card.IsRemembered | Origin$ Library | Destination$ Library | LibraryPosition$ -1 | RandomOrder$ True | SubAbility$ DBCleanup
SVar:DBRestRandomOrder:DB$ ChangeZoneAll | ChangeType$ Card.IsRemembered | Origin$ Exile | Destination$ Library | LibraryPosition$ -1 | RandomOrder$ True | SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
Oracle:Flying\nWhenever Dazzling Sphinx deals combat damage to a player, that player exiles cards from the top of their library until they exile an instant or sorcery card. You may cast that card without paying its mana cost. Then that player puts the exiled cards that weren't cast this way on the bottom of their library in a random order.

View File

@@ -760,7 +760,8 @@ public class HumanCostDecision extends CostDecisionMakerBase {
if (num == 0) {
return PaymentDecision.number(0);
}
if (hand.size() == num) {
// player might not want to pay if from a trigger
if (!ability.hasSVar("IsCastFromPlayEffect") && hand.size() == num) {
return PaymentDecision.card(hand);
}

View File

@@ -472,7 +472,7 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont
return null;
}
String announceTitle = ("X".equals(announce)) ? announce : ability.getParamOrDefault("AnnounceTitle", announce);
String announceTitle = "X".equals(announce) ? announce : ability.getParamOrDefault("AnnounceTitle", announce);
if (cost.isMandatory()) {
return chooseNumber(ability, localizer.getMessage("lblChooseAnnounceForCard", announceTitle,
CardTranslation.getTranslatedName(ability.getHostCard().getName())) , min, max);