diff --git a/forge-ai/src/main/java/forge/ai/AiAttackController.java b/forge-ai/src/main/java/forge/ai/AiAttackController.java index fa600d9bbce..9d6623523b0 100644 --- a/forge-ai/src/main/java/forge/ai/AiAttackController.java +++ b/forge-ai/src/main/java/forge/ai/AiAttackController.java @@ -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++; diff --git a/forge-ai/src/main/java/forge/ai/AiBlockController.java b/forge-ai/src/main/java/forge/ai/AiBlockController.java index 5f37ddf5716..83dac98417b 100644 --- a/forge-ai/src/main/java/forge/ai/AiBlockController.java +++ b/forge-ai/src/main/java/forge/ai/AiBlockController.java @@ -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() { @@ -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) { diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index 4b07f185213..d4c79223fc0 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -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; diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java index acb2554c4de..5131f6968b6 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java @@ -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; diff --git a/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java b/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java index 23e4601b293..5befcfaad0b 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/PlayEffect.java @@ -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")) { diff --git a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java index 45f5429b828..014f784fdd6 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -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"; diff --git a/forge-gui/res/cardsfolder/d/dazzling_sphinx.txt b/forge-gui/res/cardsfolder/d/dazzling_sphinx.txt index 09ba0184963..b8e017aaa3d 100644 --- a/forge-gui/res/cardsfolder/d/dazzling_sphinx.txt +++ b/forge-gui/res/cardsfolder/d/dazzling_sphinx.txt @@ -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. diff --git a/forge-gui/src/main/java/forge/player/HumanCostDecision.java b/forge-gui/src/main/java/forge/player/HumanCostDecision.java index 58e3e5e5919..54886c64129 100644 --- a/forge-gui/src/main/java/forge/player/HumanCostDecision.java +++ b/forge-gui/src/main/java/forge/player/HumanCostDecision.java @@ -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); } diff --git a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java index 39c02c9a9bb..1a7b26f5b8f 100644 --- a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java +++ b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java @@ -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);