mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-14 01:38:13 +00:00
Compare commits
6 Commits
cleaveKeyw
...
detainEffe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c32b84b40c | ||
|
|
6962c2cf99 | ||
|
|
b7ec60863a | ||
|
|
239cf3ece7 | ||
|
|
06781fb6ff | ||
|
|
a85f00043b |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -4,7 +4,6 @@ about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
type: 'Bug'
|
||||
|
||||
---
|
||||
|
||||
@@ -32,6 +31,7 @@ If applicable, add screenshots to help explain your problem.
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/feature_request.md
vendored
1
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -4,7 +4,6 @@ about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
type: 'Feature'
|
||||
|
||||
---
|
||||
|
||||
|
||||
4
.github/workflows/maven-publish.yml
vendored
4
.github/workflows/maven-publish.yml
vendored
@@ -129,9 +129,7 @@ jobs:
|
||||
makeLatest: true
|
||||
|
||||
- name: 🔧 Install XML tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxml2-utils
|
||||
run: sudo apt-get install -y libxml2-utils
|
||||
|
||||
- name: 🔼 Bump versionCode in root POM
|
||||
id: bump_version
|
||||
|
||||
2
.github/workflows/test-build.yaml
vendored
2
.github/workflows/test-build.yaml
vendored
@@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
java: ['17', '21']
|
||||
java: [ '17' ]
|
||||
name: Test with Java ${{ matrix.Java }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -66,9 +66,6 @@ forge-gui-mobile-dev/testAssets
|
||||
|
||||
forge-gui/res/cardsfolder/*.bat
|
||||
|
||||
# Generated changelog file
|
||||
forge-gui/release-files/CHANGES.txt
|
||||
|
||||
forge-gui/res/PerSetTrackingResults
|
||||
forge-gui/res/decks
|
||||
forge-gui/res/layouts
|
||||
@@ -90,7 +87,3 @@ forge-gui/tools/PerSetTrackingResults
|
||||
*.tiled-session
|
||||
/forge-gui/res/adventure/*.tiled-project
|
||||
/forge-gui/res/adventure/*.tiled-session
|
||||
|
||||
# Ignore python temporaries
|
||||
__pycache__
|
||||
*.pyc
|
||||
|
||||
33
.gitlab/issue_templates/Bug.md
Normal file
33
.gitlab/issue_templates/Bug.md
Normal file
@@ -0,0 +1,33 @@
|
||||
Summary
|
||||
|
||||
(Summarize the bug encountered concisely)
|
||||
|
||||
|
||||
Steps to reproduce
|
||||
|
||||
(How one can reproduce the issue - this is very important. Specific cards and specific actions especially)
|
||||
|
||||
|
||||
Which version of Forge are you on (Release, Snapshot? Desktop, Android?)
|
||||
|
||||
|
||||
What is the current bug behavior?
|
||||
|
||||
(What actually happens)
|
||||
|
||||
|
||||
What is the expected correct behavior?
|
||||
|
||||
(What you should see instead)
|
||||
|
||||
|
||||
Relevant logs and/or screenshots
|
||||
|
||||
(Paste/Attach your game.log from the crash - please use code blocks (```)) Also, provide screenshots of the current state.
|
||||
|
||||
|
||||
Possible fixes
|
||||
|
||||
(If you can, link to the line of code that might be responsible for the problem)
|
||||
|
||||
/label ~needs-investigation
|
||||
15
.gitlab/issue_templates/Feature.md
Normal file
15
.gitlab/issue_templates/Feature.md
Normal file
@@ -0,0 +1,15 @@
|
||||
Summary
|
||||
|
||||
(Summarize the feature you wish concisely)
|
||||
|
||||
|
||||
Example screenshots
|
||||
|
||||
(If this is a UI change, please provide an example screenshot of how this feature might work)
|
||||
|
||||
|
||||
Feature type
|
||||
|
||||
(Where in Forge does this belong? e.g. Quest Mode, Deck Editor, Limited, Constructed, etc.)
|
||||
|
||||
/label ~feature request
|
||||
@@ -15,7 +15,7 @@ public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
GuiBase.setInterface(new GuiMobile(Files.exists(Paths.get("./res"))?"./":"../forge-gui/"));
|
||||
GuiBase.setDeviceInfo(null, 0, 0, System.getProperty("user.home") + "/Downloads/");
|
||||
GuiBase.setDeviceInfo("", "", 0, 0);
|
||||
new EditorMainWindow(Config.instance());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ public class AiCardMemory {
|
||||
ATTACHED_THIS_TURN, // These equipments were attached to something already this turn
|
||||
ANIMATED_THIS_TURN, // These cards had their AF Animate effect activated this turn
|
||||
BOUNCED_THIS_TURN, // These cards were bounced this turn
|
||||
ACTIVATED_THIS_TURN, // These cards had their ability activated this turn
|
||||
CHOSEN_FOG_EFFECT, // These cards are marked as the Fog-like effect the AI is planning to cast this turn
|
||||
MARKED_TO_AVOID_REENTRY, // These cards may cause a stack smash when processed recursively, and are thus marked to avoid a crash
|
||||
PAYS_TAP_COST, // These cards will be tapped as part of a cost and cannot be chosen in another part
|
||||
|
||||
@@ -66,11 +66,13 @@ import io.sentry.Breadcrumb;
|
||||
import io.sentry.Sentry;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
@@ -97,7 +99,6 @@ public class AiController {
|
||||
private int lastAttackAggression;
|
||||
private boolean useLivingEnd;
|
||||
private List<SpellAbility> skipped;
|
||||
private boolean timeoutReached;
|
||||
|
||||
public AiController(final Player computerPlayer, final Game game0) {
|
||||
player = computerPlayer;
|
||||
@@ -482,7 +483,7 @@ public class AiController {
|
||||
|
||||
if (lands.size() >= Math.max(maxCmcInHand, 6)) {
|
||||
// don't play MDFC land if other side is spell and enough lands are available
|
||||
if (!c.isLand() || (c.isModal() && !c.getState(CardStateName.Backside).getType().isLand())) {
|
||||
if (!c.isLand() || (c.isModal() && !c.getState(CardStateName.Modal).getType().isLand())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -887,8 +888,27 @@ public class AiController {
|
||||
private AiPlayDecision canPlayAndPayForFace(final SpellAbility sa) {
|
||||
final Card host = sa.getHostCard();
|
||||
|
||||
if (sa.hasParam("AICheckSVar") && !aiShouldRun(sa, sa, host, null)) {
|
||||
return AiPlayDecision.AnotherTime;
|
||||
// Check a predefined condition
|
||||
if (sa.hasParam("AICheckSVar")) {
|
||||
final String svarToCheck = sa.getParam("AICheckSVar");
|
||||
String comparator = "GE";
|
||||
int compareTo = 1;
|
||||
|
||||
if (sa.hasParam("AISVarCompare")) {
|
||||
final String fullCmp = sa.getParam("AISVarCompare");
|
||||
comparator = fullCmp.substring(0, 2);
|
||||
final String strCmpTo = fullCmp.substring(2);
|
||||
try {
|
||||
compareTo = Integer.parseInt(strCmpTo);
|
||||
} catch (final Exception ignored) {
|
||||
compareTo = AbilityUtils.calculateAmount(host, host.getSVar(strCmpTo), sa);
|
||||
}
|
||||
}
|
||||
|
||||
int left = AbilityUtils.calculateAmount(host, svarToCheck, sa);
|
||||
if (!Expressions.compare(left, comparator, compareTo)) {
|
||||
return AiPlayDecision.AnotherTime;
|
||||
}
|
||||
}
|
||||
|
||||
// this is the "heaviest" check, which also sets up targets, defines X, etc.
|
||||
@@ -906,7 +926,7 @@ public class AiController {
|
||||
|
||||
// check if enough left (pass memory indirectly because we don't want to include those)
|
||||
Set<Card> tappedForMana = AiCardMemory.getMemorySet(player, MemorySet.PAYS_TAP_COST);
|
||||
if (tappedForMana != null && !tappedForMana.isEmpty() &&
|
||||
if (tappedForMana != null && tappedForMana.isEmpty() &&
|
||||
!ComputerUtilCost.checkTapTypeCost(player, sa.getPayCosts(), host, sa, new CardCollection(tappedForMana))) {
|
||||
return AiPlayDecision.CantAfford;
|
||||
}
|
||||
@@ -1646,10 +1666,8 @@ public class AiController {
|
||||
Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex);
|
||||
}
|
||||
|
||||
// in case of infinite loop reset below would not be reached
|
||||
timeoutReached = false;
|
||||
|
||||
FutureTask<SpellAbility> future = new FutureTask<>(() -> {
|
||||
final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
Future<SpellAbility> future = executor.submit(() -> {
|
||||
//avoid ComputerUtil.aiLifeInDanger in loops as it slows down a lot.. call this outside loops will generally be fast...
|
||||
boolean isLifeInDanger = useLivingEnd && ComputerUtil.aiLifeInDanger(player, true, 0);
|
||||
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) {
|
||||
@@ -1658,11 +1676,6 @@ public class AiController {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (timeoutReached) {
|
||||
timeoutReached = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (sa.getHostCard().hasKeyword(Keyword.STORM)
|
||||
&& sa.getApi() != ApiType.Counter // AI would suck at trying to deliberately proc a Storm counterspell
|
||||
&& player.getZone(ZoneType.Hand).contains(
|
||||
@@ -1732,21 +1745,11 @@ public class AiController {
|
||||
return null;
|
||||
});
|
||||
|
||||
Thread t = new Thread(future);
|
||||
t.start();
|
||||
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
|
||||
try {
|
||||
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
|
||||
return future.get(game.getAITimeout(), TimeUnit.SECONDS);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
try {
|
||||
t.stop();
|
||||
} catch (UnsupportedOperationException ex) {
|
||||
// Android and Java 20 dropped support to stop so sadly thread will keep running
|
||||
timeoutReached = true;
|
||||
future.cancel(true);
|
||||
// TODO wait a few more seconds to try and exit at a safe point before letting the engine continue
|
||||
// TODO mark some as skipped to increase chance to find something playable next priority
|
||||
}
|
||||
future.cancel(true);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1798,9 +1801,14 @@ public class AiController {
|
||||
* @param sa the sa
|
||||
* @return true, if successful
|
||||
*/
|
||||
public final boolean aiShouldRun(final CardTraitBase effect, final SpellAbility sa, final Card host, final GameEntity affected) {
|
||||
public final boolean aiShouldRun(final ReplacementEffect effect, final SpellAbility sa, GameEntity affected) {
|
||||
Card hostCard = effect.getHostCard();
|
||||
if (hostCard.hasAlternateState()) {
|
||||
hostCard = game.getCardState(hostCard);
|
||||
}
|
||||
|
||||
if (effect.hasParam("AILogic") && effect.getParam("AILogic").equalsIgnoreCase("ProtectFriendly")) {
|
||||
final Player controller = host.getController();
|
||||
final Player controller = hostCard.getController();
|
||||
if (affected instanceof Player) {
|
||||
return !((Player) affected).isOpponentOf(controller);
|
||||
}
|
||||
@@ -1809,6 +1817,7 @@ public class AiController {
|
||||
}
|
||||
}
|
||||
if (effect.hasParam("AICheckSVar")) {
|
||||
System.out.println("aiShouldRun?" + sa);
|
||||
final String svarToCheck = effect.getParam("AICheckSVar");
|
||||
String comparator = "GE";
|
||||
int compareTo = 1;
|
||||
@@ -1821,9 +1830,9 @@ public class AiController {
|
||||
compareTo = Integer.parseInt(strCmpTo);
|
||||
} catch (final Exception ignored) {
|
||||
if (sa == null) {
|
||||
compareTo = AbilityUtils.calculateAmount(host, host.getSVar(strCmpTo), effect);
|
||||
compareTo = AbilityUtils.calculateAmount(hostCard, hostCard.getSVar(strCmpTo), effect);
|
||||
} else {
|
||||
compareTo = AbilityUtils.calculateAmount(host, host.getSVar(strCmpTo), sa);
|
||||
compareTo = AbilityUtils.calculateAmount(hostCard, hostCard.getSVar(strCmpTo), sa);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1831,12 +1840,13 @@ public class AiController {
|
||||
int left = 0;
|
||||
|
||||
if (sa == null) {
|
||||
left = AbilityUtils.calculateAmount(host, svarToCheck, effect);
|
||||
left = AbilityUtils.calculateAmount(hostCard, svarToCheck, effect);
|
||||
} else {
|
||||
left = AbilityUtils.calculateAmount(host, svarToCheck, sa);
|
||||
left = AbilityUtils.calculateAmount(hostCard, svarToCheck, sa);
|
||||
}
|
||||
System.out.println("aiShouldRun?" + left + comparator + compareTo);
|
||||
return Expressions.compare(left, comparator, compareTo);
|
||||
} else if (effect.isKeyword(Keyword.DREDGE)) {
|
||||
} else if (effect.hasParam("AICheckDredge")) {
|
||||
return player.getCardsIn(ZoneType.Library).size() > 8 || player.isCardInPlay("Laboratory Maniac");
|
||||
} else return sa != null && doTrigger(sa, false);
|
||||
}
|
||||
|
||||
@@ -29,15 +29,12 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
private final CardCollection tapped;
|
||||
|
||||
public AiCostDecision(Player ai0, SpellAbility sa, final boolean effect) {
|
||||
this(ai0, sa, effect, false);
|
||||
}
|
||||
public AiCostDecision(Player ai0, SpellAbility sa, final boolean effect, final boolean payMana) {
|
||||
super(ai0, effect, sa, sa.getHostCard());
|
||||
|
||||
discarded = new CardCollection();
|
||||
tapped = new CardCollection();
|
||||
Set<Card> tappedForMana = AiCardMemory.getMemorySet(ai0, MemorySet.PAYS_TAP_COST);
|
||||
if (!payMana && tappedForMana != null) {
|
||||
if (tappedForMana != null) {
|
||||
tapped.addAll(tappedForMana);
|
||||
}
|
||||
}
|
||||
@@ -113,7 +110,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
randomSubset = ability.getActivatingPlayer().getController().orderMoveToZoneList(randomSubset, ZoneType.Graveyard, ability);
|
||||
}
|
||||
return PaymentDecision.card(randomSubset);
|
||||
} else if (type.contains("+WithDifferentNames")) {
|
||||
} else if (type.equals("DifferentNames")) {
|
||||
CardCollection differentNames = new CardCollection();
|
||||
CardCollection discardMe = CardLists.filter(hand, CardPredicates.hasSVar("DiscardMe"));
|
||||
while (c > 0) {
|
||||
@@ -566,7 +563,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
int thisRemove = Math.min(prefCard.getCounters(cType), stillToRemove);
|
||||
if (thisRemove > 0) {
|
||||
removed += thisRemove;
|
||||
table.put(null, prefCard, cType, thisRemove);
|
||||
table.put(null, prefCard, CounterType.get(cType), thisRemove);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -576,7 +573,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
@Override
|
||||
public PaymentDecision visit(CostRemoveAnyCounter cost) {
|
||||
final int c = cost.getAbilityAmount(ability);
|
||||
final Card originalHost = ObjectUtils.getIfNull(ability.getOriginalHost(), source);
|
||||
final Card originalHost = ObjectUtils.defaultIfNull(ability.getOriginalHost(), source);
|
||||
|
||||
if (c <= 0) {
|
||||
return null;
|
||||
@@ -719,7 +716,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
int over = Math.min(crd.getCounters(CounterEnumType.QUEST) - e, c - toRemove);
|
||||
if (over > 0) {
|
||||
toRemove += over;
|
||||
table.put(null, crd, CounterEnumType.QUEST, over);
|
||||
table.put(null, crd, CounterType.get(CounterEnumType.QUEST), over);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public enum AiPlayDecision {
|
||||
StackNotEmpty,
|
||||
AnotherTime,
|
||||
|
||||
// Don't play decision reasons
|
||||
// Don't play decision reasons,
|
||||
CantPlaySa,
|
||||
CantPlayAi,
|
||||
CantAfford,
|
||||
|
||||
@@ -767,7 +767,7 @@ public class ComputerUtil {
|
||||
public static CardCollection chooseUntapType(final Player ai, final String type, final Card activate, final boolean untap, final int amount, SpellAbility sa) {
|
||||
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), activate.getController(), activate, sa);
|
||||
|
||||
typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterEnumType.STUN));
|
||||
typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterType.get(CounterEnumType.STUN)));
|
||||
|
||||
if (untap) {
|
||||
typeList.remove(activate);
|
||||
@@ -1074,80 +1074,6 @@ public class ComputerUtil {
|
||||
return prevented;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is it OK to cast this for less than the Max Targets?
|
||||
* @param source the source Card
|
||||
* @return true if it's OK to cast this Card for less than the max targets
|
||||
*/
|
||||
public static boolean shouldCastLessThanMax(final Player ai, final Card source) {
|
||||
if (source.getXManaCostPaid() > 0) {
|
||||
// If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by payment resources available.
|
||||
return true;
|
||||
}
|
||||
if (aiLifeInDanger(ai, false, 0)) {
|
||||
// Otherwise, if life is possibly in danger, then this is fine.
|
||||
return true;
|
||||
}
|
||||
// do not play now.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this discard probably worse than a random draw?
|
||||
* @param discard Card to discard
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean isWorseThanDraw(final Player ai, Card discard) {
|
||||
if (discard.hasSVar("DiscardMe")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final Game game = ai.getGame();
|
||||
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
|
||||
final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS);
|
||||
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
|
||||
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, Card::getCMC));
|
||||
final int discardCMC = discard.getCMC();
|
||||
if (discard.isLand()) {
|
||||
if (landsInPlay.size() >= highestCMC
|
||||
|| (landsInPlay.size() + landsInHand.size() > 6 && landsInHand.size() > 1)
|
||||
|| (landsInPlay.size() > 3 && nonLandsInHand.size() == 0)) {
|
||||
// Don't need more land.
|
||||
return true;
|
||||
}
|
||||
} else { //non-land
|
||||
if (discardCMC > landsInPlay.size() + landsInHand.size() + 2) {
|
||||
// not castable for some time.
|
||||
return true;
|
||||
} else if (!game.getPhaseHandler().isPlayerTurn(ai)
|
||||
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.MAIN2)
|
||||
&& discardCMC > landsInPlay.size() + landsInHand.size()
|
||||
&& discardCMC > landsInPlay.size() + 1
|
||||
&& nonLandsInHand.size() > 1) {
|
||||
// not castable for at least one other turn.
|
||||
return true;
|
||||
} else if (landsInPlay.size() > 5 && discard.getCMC() <= 1
|
||||
&& !discard.hasProperty("hasXCost", ai, null, null)) {
|
||||
// Probably don't need small stuff now.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// returns true if it's better to wait until blockers are declared
|
||||
public static boolean waitForBlocking(final SpellAbility sa) {
|
||||
final Game game = sa.getActivatingPlayer().getGame();
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
|
||||
return sa.getHostCard().isCreature()
|
||||
&& sa.getPayCosts().hasTapCost()
|
||||
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
&& !ph.getNextTurn().equals(sa.getActivatingPlayer()))
|
||||
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
|
||||
&& !sa.hasParam("ActivationPhases");
|
||||
}
|
||||
|
||||
public static boolean castPermanentInMain1(final Player ai, final SpellAbility sa) {
|
||||
final Card card = sa.getHostCard();
|
||||
final CardState cardState = card.isFaceDown() ? card.getState(CardStateName.Original) : card.getCurrentState();
|
||||
@@ -1319,6 +1245,80 @@ public class ComputerUtil {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is it OK to cast this for less than the Max Targets?
|
||||
* @param source the source Card
|
||||
* @return true if it's OK to cast this Card for less than the max targets
|
||||
*/
|
||||
public static boolean shouldCastLessThanMax(final Player ai, final Card source) {
|
||||
if (source.getXManaCostPaid() > 0) {
|
||||
// If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by payment resources available.
|
||||
return true;
|
||||
}
|
||||
if (aiLifeInDanger(ai, false, 0)) {
|
||||
// Otherwise, if life is possibly in danger, then this is fine.
|
||||
return true;
|
||||
}
|
||||
// do not play now.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this discard probably worse than a random draw?
|
||||
* @param discard Card to discard
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean isWorseThanDraw(final Player ai, Card discard) {
|
||||
if (discard.hasSVar("DiscardMe")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final Game game = ai.getGame();
|
||||
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
|
||||
final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS);
|
||||
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
|
||||
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, Card::getCMC));
|
||||
final int discardCMC = discard.getCMC();
|
||||
if (discard.isLand()) {
|
||||
if (landsInPlay.size() >= highestCMC
|
||||
|| (landsInPlay.size() + landsInHand.size() > 6 && landsInHand.size() > 1)
|
||||
|| (landsInPlay.size() > 3 && nonLandsInHand.size() == 0)) {
|
||||
// Don't need more land.
|
||||
return true;
|
||||
}
|
||||
} else { //non-land
|
||||
if (discardCMC > landsInPlay.size() + landsInHand.size() + 2) {
|
||||
// not castable for some time.
|
||||
return true;
|
||||
} else if (!game.getPhaseHandler().isPlayerTurn(ai)
|
||||
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.MAIN2)
|
||||
&& discardCMC > landsInPlay.size() + landsInHand.size()
|
||||
&& discardCMC > landsInPlay.size() + 1
|
||||
&& nonLandsInHand.size() > 1) {
|
||||
// not castable for at least one other turn.
|
||||
return true;
|
||||
} else if (landsInPlay.size() > 5 && discard.getCMC() <= 1
|
||||
&& !discard.hasProperty("hasXCost", ai, null, null)) {
|
||||
// Probably don't need small stuff now.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// returns true if it's better to wait until blockers are declared
|
||||
public static boolean waitForBlocking(final SpellAbility sa) {
|
||||
final Game game = sa.getActivatingPlayer().getGame();
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
|
||||
return sa.getHostCard().isCreature()
|
||||
&& sa.getPayCosts().hasTapCost()
|
||||
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
&& !ph.getNextTurn().equals(sa.getActivatingPlayer()))
|
||||
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
|
||||
&& !sa.hasParam("ActivationPhases");
|
||||
}
|
||||
|
||||
public static boolean castSpellInMain1(final Player ai, final SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final SpellAbility sub = sa.getSubAbility();
|
||||
@@ -1327,6 +1327,7 @@ public class ComputerUtil {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cipher spells
|
||||
if (sub != null) {
|
||||
final ApiType api = sub.getApi();
|
||||
if (ApiType.Encode == api && !ai.getCreaturesInPlay().isEmpty()) {
|
||||
@@ -1621,6 +1622,7 @@ public class ComputerUtil {
|
||||
damage = dmg;
|
||||
}
|
||||
|
||||
// Triggered abilities
|
||||
if (c.isCreature() && c.isInPlay() && CombatUtil.canAttack(c)) {
|
||||
for (final Trigger t : c.getTriggers()) {
|
||||
if (TriggerType.Attacks.equals(t.getMode())) {
|
||||
@@ -2542,7 +2544,7 @@ public class ComputerUtil {
|
||||
|
||||
boolean opponent = controller.isOpponentOf(ai);
|
||||
|
||||
final CounterType p1p1Type = CounterEnumType.P1P1;
|
||||
final CounterType p1p1Type = CounterType.get(CounterEnumType.P1P1);
|
||||
|
||||
if (!sa.hasParam("AILogic")) {
|
||||
return Aggregates.random(options);
|
||||
@@ -2551,7 +2553,7 @@ public class ComputerUtil {
|
||||
String logic = sa.getParam("AILogic");
|
||||
switch (logic) {
|
||||
case "Torture":
|
||||
return options.get(1);
|
||||
return "Torture";
|
||||
case "GraceOrCondemnation":
|
||||
List<ZoneType> graceZones = new ArrayList<ZoneType>();
|
||||
graceZones.add(ZoneType.Battlefield);
|
||||
@@ -2559,12 +2561,12 @@ public class ComputerUtil {
|
||||
CardCollection graceCreatures = CardLists.getType(game.getCardsIn(graceZones), "Creature");
|
||||
int humanGrace = CardLists.filterControlledBy(graceCreatures, ai.getOpponents()).size();
|
||||
int aiGrace = CardLists.filterControlledBy(graceCreatures, ai).size();
|
||||
return options.get(aiGrace > humanGrace ? 0 : 1);
|
||||
return aiGrace > humanGrace ? "Grace" : "Condemnation";
|
||||
case "CarnageOrHomage":
|
||||
CardCollection cardsInPlay = CardLists.getNotType(game.getCardsIn(ZoneType.Battlefield), "Land");
|
||||
CardCollection humanlist = CardLists.filterControlledBy(cardsInPlay, ai.getOpponents());
|
||||
CardCollection computerlist = ai.getCreaturesInPlay();
|
||||
return options.get(ComputerUtilCard.evaluatePermanentList(computerlist) + 3 < ComputerUtilCard.evaluatePermanentList(humanlist) ? 0 : 1);
|
||||
return ComputerUtilCard.evaluatePermanentList(computerlist) + 3 < ComputerUtilCard.evaluatePermanentList(humanlist) ? "Carnage" : "Homage";
|
||||
case "Judgment":
|
||||
if (votes.isEmpty()) {
|
||||
CardCollection list = new CardCollection();
|
||||
@@ -2578,71 +2580,68 @@ public class ComputerUtil {
|
||||
return Iterables.getFirst(votes.keySet(), null);
|
||||
case "Protection":
|
||||
if (votes.isEmpty()) {
|
||||
Map<String, SpellAbility> restrictedToColors = Maps.newHashMap();
|
||||
List<String> restrictedToColors = Lists.newArrayList();
|
||||
for (Object o : options) {
|
||||
if (o instanceof SpellAbility sp) { // TODO check for Color Word Changes
|
||||
restrictedToColors.put(sp.getOriginalDescription(), sp);
|
||||
if (o instanceof String) {
|
||||
restrictedToColors.add((String) o);
|
||||
}
|
||||
}
|
||||
}
|
||||
CardCollection lists = CardLists.filterControlledBy(game.getCardsInGame(), ai.getOpponents());
|
||||
return restrictedToColors.get(StringUtils.capitalize(ComputerUtilCard.getMostProminentColor(lists, restrictedToColors.keySet())));
|
||||
return StringUtils.capitalize(ComputerUtilCard.getMostProminentColor(lists, restrictedToColors));
|
||||
}
|
||||
return Iterables.getFirst(votes.keySet(), null);
|
||||
case "FeatherOrQuill":
|
||||
SpellAbility feather = (SpellAbility)options.get(0);
|
||||
SpellAbility quill = (SpellAbility)options.get(1);
|
||||
// try to mill opponent with Quill vote
|
||||
if (opponent && !controller.cantLoseCheck(GameLossReason.Milled)) {
|
||||
int numQuill = votes.get(quill).size();
|
||||
int numQuill = votes.get("Quill").size();
|
||||
if (numQuill + 1 >= controller.getCardsIn(ZoneType.Library).size()) {
|
||||
return controller.isCardInPlay("Laboratory Maniac") ? feather : quill;
|
||||
return controller.isCardInPlay("Laboratory Maniac") ? "Feather" : "Quill";
|
||||
}
|
||||
}
|
||||
// is it can't receive counters, choose +1/+1 ones
|
||||
if (!source.canReceiveCounters(p1p1Type)) {
|
||||
return opponent ? feather : quill;
|
||||
return opponent ? "Feather" : "Quill";
|
||||
}
|
||||
// if source is not on the battlefield anymore, choose +1/+1 ones
|
||||
if (!game.getCardState(source).isInPlay()) {
|
||||
return opponent ? feather : quill;
|
||||
return opponent ? "Feather" : "Quill";
|
||||
}
|
||||
// if no hand cards, try to mill opponent
|
||||
if (controller.getCardsIn(ZoneType.Hand).isEmpty()) {
|
||||
return opponent ? quill : feather;
|
||||
return opponent ? "Quill" : "Feather";
|
||||
}
|
||||
|
||||
// AI has something to discard
|
||||
if (ai.equals(controller)) {
|
||||
CardCollectionView aiCardsInHand = ai.getCardsIn(ZoneType.Hand);
|
||||
if (CardLists.count(aiCardsInHand, CardPredicates.hasSVar("DiscardMe")) >= 1) {
|
||||
return quill;
|
||||
return "Quill";
|
||||
}
|
||||
}
|
||||
|
||||
// default card draw and discard are better than +1/+1 counter
|
||||
return opponent ? feather : quill;
|
||||
return opponent ? "Feather" : "Quill";
|
||||
case "StrengthOrNumbers":
|
||||
SpellAbility strength = (SpellAbility)options.get(0);
|
||||
SpellAbility numbers = (SpellAbility)options.get(1);
|
||||
// similar to fabricate choose +1/+1 or Token
|
||||
int numStrength = votes.get(strength).size();
|
||||
int numNumbers = votes.get(numbers).size();
|
||||
final SpellAbility saToken = sa.findSubAbilityByType(ApiType.Token);
|
||||
int numStrength = votes.get("Strength").size();
|
||||
int numNumbers = votes.get("Numbers").size();
|
||||
|
||||
Card token = TokenAi.spawnToken(controller, numbers);
|
||||
Card token = TokenAi.spawnToken(controller, saToken);
|
||||
|
||||
// is it can't receive counters, choose +1/+1 ones
|
||||
if (!source.canReceiveCounters(p1p1Type)) {
|
||||
return opponent ? strength : numbers;
|
||||
return opponent ? "Strength" : "Numbers";
|
||||
}
|
||||
|
||||
// if source is not on the battlefield anymore
|
||||
if (!game.getCardState(source).isInPlay()) {
|
||||
return opponent ? strength : numbers;
|
||||
return opponent ? "Strength" : "Numbers";
|
||||
}
|
||||
|
||||
// token would not survive
|
||||
if (token == null || !token.isCreature() || token.getNetToughness() < 1) {
|
||||
return opponent ? numbers : strength;
|
||||
return opponent ? "Numbers" : "Strength";
|
||||
}
|
||||
|
||||
// TODO check for ETB to +1/+1 counters or over another trigger like lifegain
|
||||
@@ -2663,40 +2662,35 @@ public class ComputerUtil {
|
||||
int scoreStrength = ComputerUtilCard.evaluateCreature(sourceStrength) + tokenScore * numNumbers;
|
||||
int scoreNumbers = ComputerUtilCard.evaluateCreature(sourceNumbers) + tokenScore * (numNumbers + 1);
|
||||
|
||||
return (scoreNumbers >= scoreStrength) != opponent ? numbers : strength;
|
||||
return (scoreNumbers >= scoreStrength) != opponent ? "Numbers" : "Strength";
|
||||
case "SproutOrHarvest":
|
||||
SpellAbility sprout = (SpellAbility)options.get(0);
|
||||
SpellAbility harvest = (SpellAbility)options.get(1);
|
||||
// lifegain would hurt or has no effect
|
||||
if (opponent) {
|
||||
if (lifegainNegative(controller, source)) {
|
||||
return harvest;
|
||||
return "Harvest";
|
||||
}
|
||||
} else {
|
||||
if (lifegainNegative(controller, source)) {
|
||||
return sprout;
|
||||
return "Sprout";
|
||||
}
|
||||
}
|
||||
|
||||
// is it can't receive counters, choose +1/+1 ones
|
||||
if (!source.canReceiveCounters(p1p1Type)) {
|
||||
return opponent ? sprout : harvest;
|
||||
return opponent ? "Sprout" : "Harvest";
|
||||
}
|
||||
|
||||
// if source is not on the battlefield anymore
|
||||
if (!game.getCardState(source).isInPlay()) {
|
||||
return opponent ? sprout : harvest;
|
||||
return opponent ? "Sprout" : "Harvest";
|
||||
}
|
||||
// TODO add Lifegain to +1/+1 counters trigger
|
||||
|
||||
// for now +1/+1 counters are better
|
||||
return opponent ? harvest : sprout;
|
||||
return opponent ? "Harvest" : "Sprout";
|
||||
case "DeathOrTaxes":
|
||||
SpellAbility death = (SpellAbility)options.get(0);
|
||||
SpellAbility taxes = (SpellAbility)options.get(1);
|
||||
|
||||
int numDeath = votes.get(death).size();
|
||||
int numTaxes = votes.get(taxes).size();
|
||||
int numDeath = votes.get("Death").size();
|
||||
int numTaxes = votes.get("Taxes").size();
|
||||
|
||||
if (opponent) {
|
||||
CardCollection aiCreatures = ai.getCreaturesInPlay();
|
||||
@@ -2704,29 +2698,29 @@ public class ComputerUtil {
|
||||
// would need to sacrifice more creatures than AI has
|
||||
// sacrifice even more
|
||||
if (aiCreatures.size() <= numDeath) {
|
||||
return death;
|
||||
return "Death";
|
||||
}
|
||||
// would need to discard more cards than it has
|
||||
if (aiCardsInHand.size() <= numTaxes) {
|
||||
return taxes;
|
||||
return "Taxes";
|
||||
}
|
||||
|
||||
// has cards with SacMe or Token
|
||||
if (CardLists.count(aiCreatures, CardPredicates.hasSVar("SacMe").or(CardPredicates.TOKEN)) >= numDeath) {
|
||||
return death;
|
||||
return "Death";
|
||||
}
|
||||
|
||||
// has cards with DiscardMe
|
||||
if (CardLists.count(aiCardsInHand, CardPredicates.hasSVar("DiscardMe")) >= numTaxes) {
|
||||
return taxes;
|
||||
return "Taxes";
|
||||
}
|
||||
|
||||
// discard is probably less worse than sacrifice
|
||||
return taxes;
|
||||
return "Taxes";
|
||||
} else {
|
||||
// ai is first voter or ally of controller
|
||||
// both are not affected, but if opponents control creatures, sacrifice is worse
|
||||
return controller.getOpponents().getCreaturesInPlay().isEmpty() ? taxes : death;
|
||||
return controller.getOpponents().getCreaturesInPlay().isEmpty() ? "Taxes" : "Death";
|
||||
}
|
||||
default:
|
||||
return Iterables.getFirst(options, null);
|
||||
@@ -2743,7 +2737,7 @@ public class ComputerUtil {
|
||||
return safeCards;
|
||||
}
|
||||
|
||||
public static Card getKilledByTargeting(final SpellAbility sa, CardCollectionView validCards) {
|
||||
public static Card getKilledByTargeting(final SpellAbility sa, Iterable<Card> validCards) {
|
||||
CardCollection killables = CardLists.filter(validCards, c -> c.getController() != sa.getActivatingPlayer() && c.getSVar("Targeting").equals("Dies"));
|
||||
return ComputerUtilCard.getBestCreatureAI(killables);
|
||||
}
|
||||
@@ -3104,38 +3098,41 @@ public class ComputerUtil {
|
||||
|
||||
public static CardCollection filterAITgts(SpellAbility sa, Player ai, CardCollection srcList, boolean alwaysStrict) {
|
||||
final Card source = sa.getHostCard();
|
||||
if (source == null || !sa.hasParam("AITgts")) {
|
||||
return srcList;
|
||||
}
|
||||
if (source == null) { return srcList; }
|
||||
|
||||
CardCollection list;
|
||||
String aiTgts = sa.getParam("AITgts");
|
||||
if (aiTgts.startsWith("BetterThan")) {
|
||||
int value = 0;
|
||||
if (aiTgts.endsWith("Source")) {
|
||||
value = ComputerUtilCard.evaluateCreature(source);
|
||||
if (source.isEnchanted()) {
|
||||
for (Card enc : source.getEnchantedBy()) {
|
||||
if (enc.getController().equals(ai)) {
|
||||
value += 100; // is 100 per AI's own aura enough?
|
||||
if (sa.hasParam("AITgts")) {
|
||||
CardCollection list;
|
||||
String aiTgts = sa.getParam("AITgts");
|
||||
if (aiTgts.startsWith("BetterThan")) {
|
||||
int value = 0;
|
||||
if (aiTgts.endsWith("Source")) {
|
||||
value = ComputerUtilCard.evaluateCreature(source);
|
||||
if (source.isEnchanted()) {
|
||||
for (Card enc : source.getEnchantedBy()) {
|
||||
if (enc.getController().equals(ai)) {
|
||||
value += 100; // is 100 per AI's own aura enough?
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (aiTgts.contains("EvalRating.")) {
|
||||
value = AbilityUtils.calculateAmount(source, aiTgts.substring(aiTgts.indexOf(".") + 1), sa);
|
||||
} else {
|
||||
System.err.println("Warning: Unspecified AI target evaluation rating for SA " + sa);
|
||||
value = ComputerUtilCard.evaluateCreature(source);
|
||||
}
|
||||
} else if (aiTgts.contains("EvalRating.")) {
|
||||
value = AbilityUtils.calculateAmount(source, aiTgts.substring(aiTgts.indexOf(".") + 1), sa);
|
||||
final int totalValue = value;
|
||||
list = CardLists.filter(srcList, c -> ComputerUtilCard.evaluateCreature(c) > totalValue + 30);
|
||||
} else {
|
||||
System.err.println("Warning: Unspecified AI target evaluation rating for SA " + sa);
|
||||
value = ComputerUtilCard.evaluateCreature(source);
|
||||
list = CardLists.getValidCards(srcList, sa.getParam("AITgts"), sa.getActivatingPlayer(), source, sa);
|
||||
}
|
||||
|
||||
if (!list.isEmpty() || sa.hasParam("AITgtsStrict") || alwaysStrict) {
|
||||
return list;
|
||||
} else {
|
||||
return srcList;
|
||||
}
|
||||
final int totalValue = value;
|
||||
list = CardLists.filter(srcList, c -> ComputerUtilCard.evaluateCreature(c) > totalValue + 30);
|
||||
} else {
|
||||
list = CardLists.getValidCards(srcList, sa.getParam("AITgts"), sa.getActivatingPlayer(), source, sa);
|
||||
}
|
||||
|
||||
if (!list.isEmpty() || sa.hasParam("AITgtsStrict") || alwaysStrict) {
|
||||
return list;
|
||||
}
|
||||
return srcList;
|
||||
}
|
||||
|
||||
|
||||
@@ -919,14 +919,14 @@ public class ComputerUtilCard {
|
||||
return MagicColor.Constant.WHITE; // no difference, there was no prominent color
|
||||
}
|
||||
|
||||
public static String getMostProminentColor(final CardCollectionView list, final Iterable<String> restrictedToColors) {
|
||||
public static String getMostProminentColor(final CardCollectionView list, final List<String> restrictedToColors) {
|
||||
byte colors = CardFactoryUtil.getMostProminentColorsFromList(list, restrictedToColors);
|
||||
for (byte c : MagicColor.WUBRG) {
|
||||
if ((colors & c) != 0) {
|
||||
return MagicColor.toLongString(c);
|
||||
}
|
||||
}
|
||||
return Iterables.get(restrictedToColors, 0); // no difference, there was no prominent color
|
||||
return restrictedToColors.get(0); // no difference, there was no prominent color
|
||||
}
|
||||
|
||||
public static List<String> getColorByProminence(final List<Card> list) {
|
||||
|
||||
@@ -974,13 +974,17 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
int pBonus = 0;
|
||||
if (ability.getApi() == ApiType.Pump) {
|
||||
if (!ability.hasParam("NumAtt")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
|
||||
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
|
||||
if (pBonus > 0) {
|
||||
power += pBonus;
|
||||
}
|
||||
}
|
||||
} else if (ability.getApi() == ApiType.PutCounter) {
|
||||
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
|
||||
continue;
|
||||
@@ -994,11 +998,12 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
}
|
||||
|
||||
if (pBonus > 0 && ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
power += pBonus;
|
||||
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
if (pBonus > 0) {
|
||||
power += pBonus;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1102,13 +1107,17 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
int tBonus = 0;
|
||||
if (ability.getApi() == ApiType.Pump) {
|
||||
if (!ability.hasParam("NumDef")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability);
|
||||
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability);
|
||||
if (tBonus > 0) {
|
||||
toughness += tBonus;
|
||||
}
|
||||
}
|
||||
} else if (ability.getApi() == ApiType.PutCounter) {
|
||||
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
|
||||
continue;
|
||||
@@ -1122,11 +1131,12 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
}
|
||||
|
||||
if (tBonus > 0 && ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
toughness += tBonus;
|
||||
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
|
||||
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
if (tBonus > 0) {
|
||||
toughness += tBonus;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return toughness;
|
||||
@@ -1295,7 +1305,6 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
int pBonus = 0;
|
||||
if (ability.getApi() == ApiType.Pump) {
|
||||
if (!ability.hasParam("NumAtt")) {
|
||||
continue;
|
||||
@@ -1305,8 +1314,11 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ability.getPayCosts().hasTapCost()) {
|
||||
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
|
||||
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
|
||||
if (pBonus > 0) {
|
||||
power += pBonus;
|
||||
}
|
||||
}
|
||||
} else if (ability.getApi() == ApiType.PutCounter) {
|
||||
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
|
||||
@@ -1321,14 +1333,13 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ability.getPayCosts().hasTapCost()) {
|
||||
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
if (pBonus > 0) {
|
||||
power += pBonus;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pBonus > 0 && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
power += pBonus;
|
||||
}
|
||||
}
|
||||
return power;
|
||||
}
|
||||
@@ -1519,14 +1530,16 @@ public class ComputerUtilCombat {
|
||||
if (ability.getPayCosts().hasTapCost() && !attacker.hasKeyword(Keyword.VIGILANCE)) {
|
||||
continue;
|
||||
}
|
||||
if (!ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int tBonus = 0;
|
||||
if (ability.getApi() == ApiType.Pump) {
|
||||
if (!ability.hasParam("NumDef")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability, true);
|
||||
toughness += AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability, true);
|
||||
} else if (ability.getApi() == ApiType.PutCounter) {
|
||||
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
|
||||
continue;
|
||||
@@ -1540,11 +1553,10 @@ public class ComputerUtilCombat {
|
||||
continue;
|
||||
}
|
||||
|
||||
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
}
|
||||
|
||||
if (tBonus > 0 && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
|
||||
toughness += tBonus;
|
||||
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
|
||||
if (tBonus > 0) {
|
||||
toughness += tBonus;
|
||||
}
|
||||
}
|
||||
}
|
||||
return toughness;
|
||||
|
||||
@@ -287,9 +287,7 @@ public class ComputerUtilMana {
|
||||
continue;
|
||||
}
|
||||
|
||||
int amount = ma.hasParam("Amount") ? AbilityUtils.calculateAmount(ma.getHostCard(), ma.getParam("Amount"), ma) : 1;
|
||||
if (amount <= 0) {
|
||||
// wrong gamestate for variable amount
|
||||
if (!ComputerUtilCost.checkTapTypeCost(ai, ma.getPayCosts(), ma.getHostCard(), sa, AiCardMemory.getMemorySet(ai, MemorySet.PAYS_TAP_COST))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -353,14 +351,9 @@ public class ComputerUtilMana {
|
||||
continue;
|
||||
}
|
||||
|
||||
// these should come last since they reserve the paying cards
|
||||
// (this means if a mana ability has both parts it doesn't currently undo reservations if the second part fails)
|
||||
if (!ComputerUtilCost.checkForManaSacrificeCost(ai, ma.getPayCosts(), ma, ma.isTrigger())) {
|
||||
continue;
|
||||
}
|
||||
if (!ComputerUtilCost.checkTapTypeCost(ai, ma.getPayCosts(), ma.getHostCard(), sa, AiCardMemory.getMemorySet(ai, MemorySet.PAYS_TAP_COST))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return paymentChoice;
|
||||
}
|
||||
@@ -450,6 +443,7 @@ public class ComputerUtilMana {
|
||||
manaProduced = manaProduced.replace(s, color);
|
||||
}
|
||||
} else if (saMana.hasParam("ReplaceColor")) {
|
||||
// replace color
|
||||
String color = saMana.getParam("ReplaceColor");
|
||||
if ("Chosen".equals(color)) {
|
||||
if (card.hasChosenColor()) {
|
||||
@@ -741,8 +735,7 @@ public class ComputerUtilMana {
|
||||
|
||||
if (saPayment != null && ComputerUtilCost.isSacrificeSelfCost(saPayment.getPayCosts())) {
|
||||
if (sa.getTargets() != null && sa.getTargets().contains(saPayment.getHostCard())) {
|
||||
// not a good idea to sac a card that you're targeting with the SA you're paying for
|
||||
saExcludeList.add(saPayment);
|
||||
saExcludeList.add(saPayment); // not a good idea to sac a card that you're targeting with the SA you're paying for
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -816,11 +809,11 @@ public class ComputerUtilMana {
|
||||
String manaProduced = predictManafromSpellAbility(saPayment, ai, toPay);
|
||||
payMultipleMana(cost, manaProduced, ai);
|
||||
|
||||
// remove to prevent re-usage since resources don't get consumed
|
||||
// remove from available lists
|
||||
sourcesForShards.values().removeIf(CardTraitPredicates.isHostCard(saPayment.getHostCard()));
|
||||
} else {
|
||||
final CostPayment pay = new CostPayment(saPayment.getPayCosts(), saPayment);
|
||||
if (!pay.payComputerCosts(new AiCostDecision(ai, saPayment, effect, true))) {
|
||||
if (!pay.payComputerCosts(new AiCostDecision(ai, saPayment, effect))) {
|
||||
saList.remove(saPayment);
|
||||
continue;
|
||||
}
|
||||
@@ -829,10 +822,8 @@ public class ComputerUtilMana {
|
||||
// subtract mana from mana pool
|
||||
manapool.payManaFromAbility(sa, cost, saPayment);
|
||||
|
||||
// need to consider if another use is now prevented
|
||||
if (!cost.isPaid() && saPayment.isActivatedAbility() && !saPayment.getRestrictions().canPlay(saPayment.getHostCard(), saPayment)) {
|
||||
sourcesForShards.values().removeIf(s -> s == saPayment);
|
||||
}
|
||||
// no need to remove abilities from resource map,
|
||||
// once their costs are paid and consume resources, they can not be used again
|
||||
|
||||
if (hasConverge) {
|
||||
// hack to prevent converge re-using sources
|
||||
@@ -1505,7 +1496,7 @@ public class ComputerUtilMana {
|
||||
}
|
||||
|
||||
if (!cost.isReusuableResource()) {
|
||||
for (CostPart part : cost.getCostParts()) {
|
||||
for(CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostSacrifice && !part.payCostFromSource()) {
|
||||
unpreferredCost = true;
|
||||
}
|
||||
@@ -1596,8 +1587,10 @@ public class ComputerUtilMana {
|
||||
|
||||
// don't use abilities with dangerous drawbacks
|
||||
AbilitySub sub = m.getSubAbility();
|
||||
if (sub != null && !SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) {
|
||||
continue;
|
||||
if (sub != null) {
|
||||
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
manaMap.get(ManaAtom.GENERIC).add(m); // add to generic source list
|
||||
@@ -1665,6 +1658,7 @@ public class ComputerUtilMana {
|
||||
if (replaced.contains("C")) {
|
||||
manaMap.put(ManaAtom.COLORLESS, m);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,14 +264,12 @@ public abstract class GameState {
|
||||
}
|
||||
|
||||
if (c.hasMergedCard()) {
|
||||
String suffix = c.getTopMergedCard().hasPaperFoil() ? "+" : "";
|
||||
// we have to go by the current top card name here
|
||||
newText.append(c.getTopMergedCard().getPaperCard().getName()).append(suffix).append("|Set:")
|
||||
newText.append(c.getTopMergedCard().getPaperCard().getName()).append("|Set:")
|
||||
.append(c.getTopMergedCard().getPaperCard().getEdition()).append("|Art:")
|
||||
.append(c.getTopMergedCard().getPaperCard().getArtIndex());
|
||||
} else {
|
||||
String suffix = c.hasPaperFoil() ? "+" : "";
|
||||
newText.append(c.getPaperCard().getName()).append(suffix).append("|Set:").append(c.getPaperCard().getEdition())
|
||||
newText.append(c.getPaperCard().getName()).append("|Set:").append(c.getPaperCard().getEdition())
|
||||
.append("|Art:").append(c.getPaperCard().getArtIndex());
|
||||
}
|
||||
}
|
||||
@@ -321,21 +319,18 @@ public abstract class GameState {
|
||||
newText.append(":Cloaked");
|
||||
}
|
||||
}
|
||||
if (c.getCurrentStateName().equals(CardStateName.Flipped)) {
|
||||
if (c.getCurrentStateName().equals(CardStateName.Transformed)) {
|
||||
newText.append("|Transformed");
|
||||
} else if (c.getCurrentStateName().equals(CardStateName.Flipped)) {
|
||||
newText.append("|Flipped");
|
||||
} else if (c.getCurrentStateName().equals(CardStateName.Meld)) {
|
||||
newText.append("|Meld");
|
||||
if (c.getMeldedWith() != null) {
|
||||
String suffix = c.getMeldedWith().hasPaperFoil() ? "+" : "";
|
||||
newText.append(":");
|
||||
newText.append(c.getMeldedWith().getName()).append(suffix);
|
||||
}
|
||||
} else if (c.getCurrentStateName().equals(CardStateName.Backside)) {
|
||||
if (c.isModal()) {
|
||||
newText.append("|Modal");
|
||||
} else {
|
||||
newText.append("|Transformed");
|
||||
newText.append(c.getMeldedWith().getName());
|
||||
}
|
||||
} else if (c.getCurrentStateName().equals(CardStateName.Modal)) {
|
||||
newText.append("|Modal");
|
||||
}
|
||||
|
||||
if (c.getPlayerAttachedTo() != null) {
|
||||
@@ -1316,8 +1311,8 @@ public abstract class GameState {
|
||||
if (info.endsWith("Cloaked")) {
|
||||
c.setCloaked(new SpellAbility.EmptySa(ApiType.Cloak, c));
|
||||
}
|
||||
} else if (info.startsWith("Transformed") || info.startsWith("Modal")) {
|
||||
c.setState(CardStateName.Backside, true);
|
||||
} else if (info.startsWith("Transformed")) {
|
||||
c.setState(CardStateName.Transformed, true);
|
||||
c.setBackSide(true);
|
||||
} else if (info.startsWith("Flipped")) {
|
||||
c.setState(CardStateName.Flipped, true);
|
||||
@@ -1335,6 +1330,9 @@ public abstract class GameState {
|
||||
}
|
||||
c.setState(CardStateName.Meld, true);
|
||||
c.setBackSide(true);
|
||||
} else if (info.startsWith("Modal")) {
|
||||
c.setState(CardStateName.Modal, true);
|
||||
c.setBackSide(true);
|
||||
}
|
||||
else if (info.startsWith("OnAdventure")) {
|
||||
String abAdventure = "DB$ Effect | RememberObjects$ Self | StaticAbilities$ Play | ForgetOnMoved$ Exile | Duration$ Permanent | ConditionDefined$ Self | ConditionPresent$ Card.!copiedSpell";
|
||||
|
||||
@@ -460,11 +460,7 @@ public class PlayerControllerAi extends PlayerController {
|
||||
|
||||
@Override
|
||||
public boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, GameEntity affected, String question) {
|
||||
Card host = replacementEffect.getHostCard();
|
||||
if (host.hasAlternateState()) {
|
||||
host = host.getGame().getCardState(host);
|
||||
}
|
||||
return brains.aiShouldRun(replacementEffect, effectSA, host, affected);
|
||||
return brains.aiShouldRun(replacementEffect, effectSA, affected);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1351,11 +1347,6 @@ public class PlayerControllerAi extends PlayerController {
|
||||
// Ai won't understand that anyway
|
||||
}
|
||||
|
||||
@Override
|
||||
public void revealUnsupported(Map<Player, List<PaperCard>> unsupported) {
|
||||
// Ai won't understand that anyway
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<DeckSection, List<? extends PaperCard>> complainCardsCantPlayWell(Deck myDeck) {
|
||||
// TODO check if profile detection set to Auto
|
||||
|
||||
@@ -171,7 +171,7 @@ public class SpecialAiLogic {
|
||||
final boolean isInfect = source.hasKeyword(Keyword.INFECT); // Flesh-Eater Imp
|
||||
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
|
||||
|
||||
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterEnumType.POISON)) {
|
||||
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
|
||||
lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent
|
||||
}
|
||||
|
||||
@@ -277,7 +277,7 @@ public class SpecialAiLogic {
|
||||
final boolean isInfect = source.hasKeyword(Keyword.INFECT);
|
||||
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
|
||||
|
||||
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterEnumType.POISON)) {
|
||||
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
|
||||
lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent
|
||||
}
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ public abstract class SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
if (!"Once".equals(aiLogic)) {
|
||||
return !sa.getHostCard().getAbilityActivatedThisTurn().getActivators(sa).contains(ai);
|
||||
return !AiCardMemory.isRememberedCard(ai, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -153,7 +153,7 @@ public abstract class SpellAbilityAi {
|
||||
* The rest of the logic not covered by the canPlayAI template is defined here
|
||||
*/
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
if (sa.getActivationsThisTurn() == 0 || MyRandom.getRandom().nextFloat() < .8f) {
|
||||
if (MyRandom.getRandom().nextFloat() < .8f) {
|
||||
// 80% chance to play the ability
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ public enum SpellApiToAi {
|
||||
.put(ApiType.DelayedTrigger, DelayedTriggerAi.class)
|
||||
.put(ApiType.Destroy, DestroyAi.class)
|
||||
.put(ApiType.DestroyAll, DestroyAllAi.class)
|
||||
.put(ApiType.Detain, DetainAi.class)
|
||||
.put(ApiType.Dig, DigAi.class)
|
||||
.put(ApiType.DigMultiple, DigMultipleAi.class)
|
||||
.put(ApiType.DigUntil, DigUntilAi.class)
|
||||
|
||||
@@ -83,7 +83,7 @@ public class AddTurnAi extends SpellAbilityAi {
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||
return doTriggerNoCost(aiPlayer, sa, false);
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -159,78 +159,81 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (sa.costHasManaX() && sa.getSVar("X").equals("Count$xPaid")) {
|
||||
// Set PayX here to maximum value.
|
||||
final int xPay = ComputerUtilCost.getMaxXValue(sa, aiPlayer, sa.isTrigger());
|
||||
|
||||
sa.setXManaCostPaid(xPay);
|
||||
}
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
return animateTgtAI(sa);
|
||||
}
|
||||
if (!sa.usesTargeting()) {
|
||||
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
boolean bFlag = false;
|
||||
boolean givesHaste = sa.hasParam("Keywords") && sa.getParam("Keywords").contains("Haste");
|
||||
for (final Card c : defined) {
|
||||
bFlag |= !c.isCreature() && !c.isTapped()
|
||||
&& (!c.hasSickness() || givesHaste || !ph.isPlayerTurn(aiPlayer))
|
||||
&& !c.isEquipping();
|
||||
|
||||
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
boolean bFlag = false;
|
||||
boolean givesHaste = sa.hasParam("Keywords") && sa.getParam("Keywords").contains("Haste");
|
||||
for (final Card c : defined) {
|
||||
bFlag |= !c.isCreature() && !c.isTapped()
|
||||
&& (!c.hasSickness() || givesHaste || !ph.isPlayerTurn(aiPlayer))
|
||||
&& !c.isEquipping();
|
||||
|
||||
// for creatures that could be improved (like Figure of Destiny)
|
||||
if (!bFlag && c.isCreature() && ("Permanent".equals(sa.getParam("Duration")) || (!c.isTapped() && !c.isSick()))) {
|
||||
int power = -5;
|
||||
if (sa.hasParam("Power")) {
|
||||
power = AbilityUtils.calculateAmount(c, sa.getParam("Power"), sa);
|
||||
}
|
||||
int toughness = -5;
|
||||
if (sa.hasParam("Toughness")) {
|
||||
toughness = AbilityUtils.calculateAmount(c, sa.getParam("Toughness"), sa);
|
||||
}
|
||||
if (sa.hasParam("Keywords")) {
|
||||
for (String keyword : sa.getParam("Keywords").split(" & ")) {
|
||||
if (!c.hasKeyword(keyword)) {
|
||||
bFlag = true;
|
||||
// for creatures that could be improved (like Figure of Destiny)
|
||||
if (!bFlag && c.isCreature() && ("Permanent".equals(sa.getParam("Duration")) || (!c.isTapped() && !c.isSick()))) {
|
||||
int power = -5;
|
||||
if (sa.hasParam("Power")) {
|
||||
power = AbilityUtils.calculateAmount(c, sa.getParam("Power"), sa);
|
||||
}
|
||||
int toughness = -5;
|
||||
if (sa.hasParam("Toughness")) {
|
||||
toughness = AbilityUtils.calculateAmount(c, sa.getParam("Toughness"), sa);
|
||||
}
|
||||
if (sa.hasParam("Keywords")) {
|
||||
for (String keyword : sa.getParam("Keywords").split(" & ")) {
|
||||
if (!c.hasKeyword(keyword)) {
|
||||
bFlag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (power + toughness > c.getCurrentPower() + c.getCurrentToughness()) {
|
||||
if (!c.isTapped() || (ph.inCombat() && game.getCombat().isAttacking(c))) {
|
||||
bFlag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSorcerySpeed(sa, aiPlayer) && !"Permanent".equals(sa.getParam("Duration"))) {
|
||||
if (sa.isCrew() && c.isCreature()) {
|
||||
// Do not try to crew a vehicle which is already a creature
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
Card animatedCopy = becomeAnimated(c, sa);
|
||||
if (ph.isPlayerTurn(aiPlayer)
|
||||
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(aiPlayer, animatedCopy)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
|
||||
}
|
||||
if (ph.getPlayerTurn().isOpponentOf(aiPlayer)
|
||||
&& !ComputerUtilCard.doesSpecifiedCreatureBlock(aiPlayer, animatedCopy)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
|
||||
}
|
||||
// also check if maybe there are static effects applied to the animated copy that would matter
|
||||
// (e.g. Myth Realized)
|
||||
if (animatedCopy.getCurrentPower() + animatedCopy.getCurrentToughness() >
|
||||
c.getCurrentPower() + c.getCurrentToughness()) {
|
||||
if (!isAnimatedThisTurn(aiPlayer, source)) {
|
||||
if (power + toughness > c.getCurrentPower() + c.getCurrentToughness()) {
|
||||
if (!c.isTapped() || (ph.inCombat() && game.getCombat().isAttacking(c))) {
|
||||
bFlag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSorcerySpeed(sa, aiPlayer) && !"Permanent".equals(sa.getParam("Duration"))) {
|
||||
if (sa.isCrew() && c.isCreature()) {
|
||||
// Do not try to crew a vehicle which is already a creature
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
Card animatedCopy = becomeAnimated(c, sa);
|
||||
if (ph.isPlayerTurn(aiPlayer)
|
||||
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(aiPlayer, animatedCopy)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
|
||||
}
|
||||
if (ph.getPlayerTurn().isOpponentOf(aiPlayer)
|
||||
&& !ComputerUtilCard.doesSpecifiedCreatureBlock(aiPlayer, animatedCopy)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
|
||||
}
|
||||
// also check if maybe there are static effects applied to the animated copy that would matter
|
||||
// (e.g. Myth Realized)
|
||||
if (animatedCopy.getCurrentPower() + animatedCopy.getCurrentToughness() >
|
||||
c.getCurrentPower() + c.getCurrentToughness()) {
|
||||
if (!isAnimatedThisTurn(aiPlayer, sa.getHostCard())) {
|
||||
if (!c.isTapped() || (ph.inCombat() && game.getCombat().isAttacking(c))) {
|
||||
bFlag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bFlag) {
|
||||
rememberAnimatedThisTurn(aiPlayer, sa.getHostCard());
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
return animateTgtAI(sa);
|
||||
}
|
||||
if (bFlag) {
|
||||
rememberAnimatedThisTurn(aiPlayer, source);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -252,7 +255,8 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
return decision;
|
||||
} else if (!mandatory) {
|
||||
return decision;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
// fallback if animate is mandatory
|
||||
sa.resetTargets();
|
||||
List<Card> list = CardUtil.getValidCardsToTarget(sa);
|
||||
@@ -273,13 +277,8 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
private AiAbilityDecision animateTgtAI(final SpellAbility sa) {
|
||||
if (sa.getMaxTargets() == 0) {
|
||||
// this happens if an optional cost is skipped, e.g. Brave the Wilds
|
||||
return new AiAbilityDecision(80, AiPlayDecision.WillPlay);
|
||||
}
|
||||
final Player ai = sa.getActivatingPlayer();
|
||||
final Game game = ai.getGame();
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
final PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||
final boolean alwaysActivatePWAbility = sa.isPwAbility()
|
||||
&& sa.getPayCosts().hasSpecificCostType(CostPutCounter.class)
|
||||
@@ -291,8 +290,10 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
types.addAll(Arrays.asList(sa.getParam("Types").split(",")));
|
||||
}
|
||||
|
||||
final Game game = ai.getGame();
|
||||
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa);
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, list, false);
|
||||
|
||||
// list is empty, no possible targets
|
||||
@@ -397,8 +398,7 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (logic.equals("SetPT")) {
|
||||
// TODO: 1. Teach the AI to use this to save the creature from direct damage;
|
||||
// 2. Determine the best target in a smarter way?
|
||||
// TODO: 1. Teach the AI to use this to save the creature from direct damage; 2. Determine the best target in a smarter way?
|
||||
Card worst = ComputerUtilCard.getWorstCreatureAI(ai.getCreaturesInPlay());
|
||||
Card buffed = becomeAnimated(worst, sa);
|
||||
|
||||
@@ -435,9 +435,11 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
if (sa.hasParam("AITgts") && !list.isEmpty()) {
|
||||
//No logic, but we do have preferences. Pick the best among those?
|
||||
Card best = ComputerUtilCard.getBestAI(list);
|
||||
sa.getTargets().add(best);
|
||||
rememberAnimatedThisTurn(ai, best);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
if(best != null) {
|
||||
sa.getTargets().add(best);
|
||||
rememberAnimatedThisTurn(ai, best);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or
|
||||
|
||||
@@ -988,7 +988,6 @@ public class AttachAi extends SpellAbilityAi {
|
||||
}
|
||||
} else if ("Remembered".equals(sa.getParam("Defined")) && sa.getParent() != null
|
||||
&& sa.getParent().getApi() == ApiType.Token && sa.getParent().hasParam("RememberTokens")) {
|
||||
// Living Weapon or similar
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
|
||||
@@ -101,7 +101,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
sa.getHostCard().removeSVar("AIPreferenceOverride");
|
||||
}
|
||||
|
||||
if (aiLogic.equals("SurpriseBlock")) {
|
||||
if (aiLogic.equals("BeforeCombat")) {
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_BEGIN)) {
|
||||
return false;
|
||||
}
|
||||
} else if (aiLogic.equals("SurpriseBlock")) {
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
return false;
|
||||
}
|
||||
@@ -285,7 +289,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
* @return a boolean.
|
||||
*/
|
||||
private static AiAbilityDecision hiddenOriginCanPlayAI(final Player ai, final SpellAbility sa) {
|
||||
// Fetching should occur fairly often as it helps cast more spells, and have access to more mana
|
||||
// Fetching should occur fairly often as it helps cast more spells, and
|
||||
// have access to more mana
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final Card source = sa.getHostCard();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
final String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
@@ -294,7 +300,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
boolean activateForCost = ComputerUtil.activateForCost(sa, ai);
|
||||
|
||||
if (sa.hasParam("Origin")) {
|
||||
origin = ZoneType.listValueOf(sa.getParam("Origin"));
|
||||
try {
|
||||
origin = ZoneType.listValueOf(sa.getParam("Origin"));
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// This happens when Origin is something like
|
||||
// "Graveyard,Library" (Doomsday)
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
final String destination = sa.getParam("Destination");
|
||||
|
||||
@@ -443,7 +455,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
final AbilitySub subAb = sa.getSubAbility();
|
||||
if (subAb == null) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -489,6 +506,19 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
// Fetching should occur fairly often as it helps cast more spells, and
|
||||
// have access to more mana
|
||||
|
||||
if (sa.hasParam("AILogic")) {
|
||||
if (sa.getParam("AILogic").equals("Never")) {
|
||||
/*
|
||||
* Hack to stop AI from using Aviary Mechanic's "may bounce" trigger.
|
||||
* Ideally it should look for a good bounce target like "Pacifism"-victims
|
||||
* but there is no simple way to check that. It is preferable for the AI
|
||||
* to make sub-optimal choices (waste bounce) than to make obvious mistakes
|
||||
* (bounce useful permanent).
|
||||
*/
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
List<ZoneType> origin = new ArrayList<>();
|
||||
if (sa.hasParam("Origin")) {
|
||||
origin = ZoneType.listValueOf(sa.getParam("Origin"));
|
||||
@@ -743,7 +773,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
final AbilitySub subAb = sa.getSubAbility();
|
||||
if (subAb == null) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -761,8 +796,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN);
|
||||
} else if (aiLogic.equals("Main1") && ph.is(PhaseType.MAIN1, ai)) {
|
||||
return true;
|
||||
} else if (aiLogic.equals("BeforeCombat")) {
|
||||
return !ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_BEGIN);
|
||||
}
|
||||
|
||||
if (sa.isHidden()) {
|
||||
@@ -889,6 +922,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(origin), sa);
|
||||
|
||||
list = ComputerUtil.filterAITgts(sa, ai, list, true);
|
||||
if (sa.hasParam("AITgtsOnlyBetterThanSelf")) {
|
||||
list = CardLists.filter(list, card -> ComputerUtilCard.evaluateCreature(card) > ComputerUtilCard.evaluateCreature(source) + 30);
|
||||
}
|
||||
|
||||
if (source.isInZone(ZoneType.Hand)) {
|
||||
list = CardLists.filter(list, CardPredicates.nameNotEquals(source.getName())); // Don't get the same card back.
|
||||
@@ -897,6 +933,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
list.remove(source); // spells can't target their own source, because it's actually in the stack zone
|
||||
}
|
||||
|
||||
// list = CardLists.canSubsequentlyTarget(list, sa);
|
||||
|
||||
if (sa.hasParam("AttachedTo")) {
|
||||
list = CardLists.filter(list, c -> {
|
||||
for (Card card : game.getCardsIn(ZoneType.Battlefield)) {
|
||||
@@ -1239,12 +1277,53 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
// if max CMC exceeded, do not choose this card (but keep looking for other options)
|
||||
if (sa.hasParam("MaxTotalTargetCMC")) {
|
||||
if (choice.getCMC() > sa.getTargetRestrictions().getMaxTotalCMC(choice, sa) - sa.getTargets().getTotalTargetedCMC()) {
|
||||
list.remove(choice);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// if max power exceeded, do not choose this card (but keep looking for other options)
|
||||
if (sa.hasParam("MaxTotalTargetPower")) {
|
||||
if (choice.getNetPower() > sa.getTargetRestrictions().getMaxTotalPower(choice, sa) -sa.getTargets().getTotalTargetedPower()) {
|
||||
list.remove(choice);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// honor the Same Creature Type restriction
|
||||
if (sa.getTargetRestrictions().isWithSameCreatureType()) {
|
||||
Card firstTarget = sa.getTargetCard();
|
||||
if (firstTarget != null && !choice.sharesCreatureTypeWith(firstTarget)) {
|
||||
list.remove(choice);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
list.remove(choice);
|
||||
if (sa.canTarget(choice)) {
|
||||
sa.getTargets().add(choice);
|
||||
}
|
||||
}
|
||||
|
||||
// Honor the Single Zone restriction. For now, simply remove targets that do not belong to the same zone as the first targeted card.
|
||||
// TODO: ideally the AI should consider at this point which targets exactly to pick (e.g. one card in the first player's graveyard
|
||||
// vs. two cards in the second player's graveyard, which cards are more relevant to be targeted, etc.). Consider improving.
|
||||
if (sa.getTargetRestrictions().isSingleZone()) {
|
||||
Card firstTgt = sa.getTargetCard();
|
||||
CardCollection toRemove = new CardCollection();
|
||||
if (firstTgt != null) {
|
||||
for (Card t : sa.getTargets().getTargetCards()) {
|
||||
if (!t.getController().equals(firstTgt.getController())) {
|
||||
toRemove.add(t);
|
||||
}
|
||||
}
|
||||
sa.getTargets().removeAll(toRemove);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1540,7 +1619,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
} else if (logic.startsWith("ExilePreference")) {
|
||||
return doExilePreferenceLogic(decider, sa, fetchList);
|
||||
} else if (logic.equals("BounceOwnTrigger")) {
|
||||
return doBounceOwnTriggerLogic(decider, sa, fetchList);
|
||||
return doBounceOwnTriggerLogic(decider, fetchList);
|
||||
}
|
||||
}
|
||||
if (fetchList.isEmpty()) {
|
||||
@@ -2104,18 +2183,16 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return AiCardMemory.isRememberedCard(ai, c, AiCardMemory.MemorySet.BOUNCED_THIS_TURN);
|
||||
}
|
||||
|
||||
private static Card doBounceOwnTriggerLogic(Player ai, SpellAbility sa, CardCollection choices) {
|
||||
private static Card doBounceOwnTriggerLogic(Player ai, CardCollection choices) {
|
||||
CardCollection unprefChoices = CardLists.filter(choices, c -> !c.isToken() && c.getOwner().equals(ai));
|
||||
// TODO check for threatened cards
|
||||
CardCollection prefChoices = CardLists.filter(unprefChoices, c -> c.hasETBTrigger(false));
|
||||
if (!prefChoices.isEmpty()) {
|
||||
return ComputerUtilCard.getBestAI(prefChoices);
|
||||
}
|
||||
if (!unprefChoices.isEmpty() && sa.getSubAbility() != null) {
|
||||
// some extra benefit like First Responder
|
||||
} else if (!unprefChoices.isEmpty()) {
|
||||
return ComputerUtilCard.getWorstAI(unprefChoices);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -286,6 +286,8 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, final SpellAbility sa, boolean mandatory) {
|
||||
// Change Zone All, can be any type moving from one zone to another
|
||||
|
||||
final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
|
||||
final ZoneType origin = ZoneType.listValueOf(sa.getParam("Origin")).get(0);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import forge.game.player.Player;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.util.Aggregates;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.collect.FCollection;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -88,7 +89,12 @@ public class CharmAi extends SpellAbilityAi {
|
||||
CharmEffect.chainAbilities(sa, chosenList);
|
||||
}
|
||||
|
||||
return super.checkApiLogic(ai, sa);
|
||||
// prevent run-away activations - first time will always return true
|
||||
if (MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn())) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
private List<AbilitySub> chooseOptionsAi(SpellAbility sa, List<AbilitySub> choices, final Player ai, boolean isTrigger, int num, int min) {
|
||||
|
||||
@@ -78,8 +78,10 @@ public class ChooseCardNameAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (mandatory) {
|
||||
// If mandatory, then we will play it.
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// If not mandatory, then we won't play it.
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,8 @@ public class ChooseNumberAi extends SpellAbilityAi {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} {
|
||||
return canPlay(ai, sa);
|
||||
}
|
||||
return canPlay(ai, sa);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import java.util.Map;
|
||||
public class CloneAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final Game game = source.getGame();
|
||||
|
||||
@@ -38,6 +38,10 @@ public class CloneAi extends SpellAbilityAi {
|
||||
// TODO - add some kind of check for during human turn to answer
|
||||
// "Can I use this to block something?"
|
||||
|
||||
if (!checkPhaseRestrictions(ai, sa, game.getPhaseHandler())) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingPhaseRestrictions);
|
||||
}
|
||||
|
||||
PhaseHandler phase = game.getPhaseHandler();
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
@@ -73,7 +77,7 @@ public class CloneAi extends SpellAbilityAi {
|
||||
|
||||
return useAbility ? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
|
||||
: new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} // end cloneCanPlayAI()
|
||||
|
||||
@Override
|
||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
@@ -96,10 +100,6 @@ public class CloneAi extends SpellAbilityAi {
|
||||
if (sa.usesTargeting()) {
|
||||
chance = cloneTgtAI(sa);
|
||||
} else {
|
||||
if (sa.isReplacementAbility() && host.isCloned()) {
|
||||
// prevent StackOverflow from infinite loop copying another ETB RE
|
||||
return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
|
||||
}
|
||||
if (sa.hasParam("Choices")) {
|
||||
CardCollectionView choices = CardLists.getValidCards(host.getGame().getCardsIn(ZoneType.Battlefield),
|
||||
sa.getParam("Choices"), host.getController(), host, sa);
|
||||
@@ -192,7 +192,7 @@ public class CloneAi extends SpellAbilityAi {
|
||||
final boolean canCloneLegendary = "True".equalsIgnoreCase(sa.getParam("NonLegendary"));
|
||||
|
||||
String filter = !isVesuva ? "Permanent.YouDontCtrl,Permanent.nonLegendary"
|
||||
: "Permanent.YouDontCtrl+!named" + name + ",Permanent.nonLegendary+!named" + name;
|
||||
: "Permanent.YouDontCtrl+notnamed" + name + ",Permanent.nonLegendary+notnamed" + name;
|
||||
|
||||
// TODO: rewrite this block so that this is done somehow more elegantly
|
||||
if (canCloneLegendary) {
|
||||
|
||||
@@ -119,7 +119,7 @@ public class ConniveAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(
|
||||
sa.isTargetNumberValid() ? 100 : 0,
|
||||
sa.isTargetNumberValid() && !sa.getTargets().isEmpty() ? 100 : 0,
|
||||
sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.TargetingFailed
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,15 +53,17 @@ public class ControlExchangeAi extends SpellAbilityAi {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
} else if (mandatory) {
|
||||
AiAbilityDecision decision = chkDrawback(sa, aiPlayer);
|
||||
if (sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return decision;
|
||||
} else {
|
||||
return canPlay(aiPlayer, sa);
|
||||
if (mandatory) {
|
||||
AiAbilityDecision decision = chkDrawback(sa, aiPlayer);
|
||||
if (sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return decision;
|
||||
} else {
|
||||
return canPlay(aiPlayer, sa);
|
||||
}
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
||||
// Not at EOT phase
|
||||
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
|
||||
}
|
||||
} else if ("DuplicatePerms".equals(aiLogic)) {
|
||||
} if ("DuplicatePerms".equals(aiLogic)) {
|
||||
final List<Card> valid = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
if (valid.size() < 2) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
@@ -212,7 +212,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
||||
if (mandatory) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (!MyRandom.percentTrue(chance)
|
||||
&& !"Always".equals(logic)
|
||||
&& !"AlwaysIfViable".equals(logic)
|
||||
&& !"AlwaysCopyActivatedAbilities".equals(logic)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
@@ -93,6 +93,7 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
|
||||
}
|
||||
if (decision == AiPlayDecision.WillPlay) {
|
||||
sa.getTargets().add(top);
|
||||
AiCardMemory.rememberCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, decision);
|
||||
@@ -114,6 +115,7 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (logic.contains("Always")) {
|
||||
// If the logic is "Always" or "AlwaysIfViable", we will always play this ability
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@@ -124,11 +126,10 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
|
||||
public AiAbilityDecision chkDrawback(final SpellAbility sa, final Player aiPlayer) {
|
||||
if ("ChainOfSmog".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.ChainOfSmog.consider(aiPlayer, sa);
|
||||
}
|
||||
if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
|
||||
} else if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
|
||||
return SpecialCardAi.ChainOfAcid.consider(aiPlayer, sa);
|
||||
}
|
||||
|
||||
}
|
||||
AiAbilityDecision decision = canPlay(aiPlayer, sa);
|
||||
if (!decision.willingToPlay()) {
|
||||
if (sa.isMandatory()) {
|
||||
|
||||
@@ -102,7 +102,7 @@ public abstract class CountersAi extends SpellAbilityAi {
|
||||
} else if (type.equals("DIVINITY")) {
|
||||
final CardCollection boon = CardLists.filter(list, c -> c.getCounters(CounterEnumType.DIVINITY) == 0);
|
||||
choice = ComputerUtilCard.getMostExpensivePermanentAI(boon);
|
||||
} else if (CounterType.getType(type).isKeywordCounter()) {
|
||||
} else if (CounterType.get(type).isKeywordCounter()) {
|
||||
choice = ComputerUtilCard.getBestCreatureAI(CardLists.getNotKeyword(list, type));
|
||||
} else {
|
||||
// The AI really should put counters on cards that can use it.
|
||||
|
||||
@@ -20,40 +20,41 @@ public class CountersMultiplyAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
if (sa.usesTargeting()) {
|
||||
return setTargets(ai, sa);
|
||||
}
|
||||
|
||||
final CounterType counterType = getCounterType(sa);
|
||||
// defined are mostly Self or Creatures you control
|
||||
CardCollection list = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
|
||||
|
||||
list = CardLists.filter(list, c -> {
|
||||
if (!c.hasCounters()) {
|
||||
return false;
|
||||
}
|
||||
if (!sa.usesTargeting()) {
|
||||
// defined are mostly Self or Creatures you control
|
||||
CardCollection list = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
|
||||
|
||||
if (counterType != null) {
|
||||
if (c.getCounters(counterType) <= 0) {
|
||||
list = CardLists.filter(list, c -> {
|
||||
if (!c.hasCounters()) {
|
||||
return false;
|
||||
}
|
||||
if (!c.canReceiveCounters(counterType)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
for (Map.Entry<CounterType, Integer> e : c.getCounters().entrySet()) {
|
||||
// has negative counter it would double
|
||||
if (ComputerUtil.isNegativeCounter(e.getKey(), c)) {
|
||||
|
||||
if (counterType != null) {
|
||||
if (c.getCounters(counterType) <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (!c.canReceiveCounters(counterType)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
for (Map.Entry<CounterType, Integer> e : c.getCounters().entrySet()) {
|
||||
// has negative counter it would double
|
||||
if (ComputerUtil.isNegativeCounter(e.getKey(), c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (list.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (list.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
} else {
|
||||
return setTargets(ai, sa);
|
||||
}
|
||||
|
||||
return super.checkApiLogic(ai, sa);
|
||||
@@ -154,7 +155,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (counterType == null || counterType.is(type)) {
|
||||
addTargetsByCounterType(ai, sa, aiList, type);
|
||||
addTargetsByCounterType(ai, sa, aiList, CounterType.get(type));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,7 +164,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
|
||||
if (!oppList.isEmpty()) {
|
||||
// not enough targets
|
||||
if (sa.canAddMoreTarget()) {
|
||||
final CounterType type = CounterEnumType.M1M1;
|
||||
final CounterType type = CounterType.get(CounterEnumType.M1M1);
|
||||
if (counterType == null || counterType == type) {
|
||||
addTargetsByCounterType(ai, sa, oppList, type);
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ public class CountersProliferateAi extends SpellAbilityAi {
|
||||
public <T extends GameEntity> T chooseSingleEntity(Player ai, SpellAbility sa, Collection<T> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
|
||||
// Proliferate is always optional for all, no need to select best
|
||||
|
||||
final CounterType poison = CounterEnumType.POISON;
|
||||
final CounterType poison = CounterType.get(CounterEnumType.POISON);
|
||||
|
||||
boolean aggroAI = (((PlayerControllerAi) ai.getController()).getAi()).getBooleanProperty(AiProps.PLAY_AGGRO);
|
||||
// because countertype can't be chosen anymore, only look for poison counters
|
||||
|
||||
@@ -92,8 +92,9 @@ public class CountersPutAi extends CountersAi {
|
||||
return false;
|
||||
}
|
||||
return chance > MyRandom.getRandom().nextFloat();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sa.isKeyword(Keyword.LEVEL_UP)) {
|
||||
@@ -123,6 +124,7 @@ public class CountersPutAi extends CountersAi {
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final Card source = sa.getHostCard();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
CardCollection list;
|
||||
Card choice = null;
|
||||
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
|
||||
final boolean divided = sa.isDividedAsYouChoose();
|
||||
@@ -168,7 +170,7 @@ public class CountersPutAi extends CountersAi {
|
||||
CardCollection oppCreatM1 = CardLists.filter(oppCreat, CardPredicates.hasCounter(CounterEnumType.M1M1));
|
||||
oppCreatM1 = CardLists.getNotKeyword(oppCreatM1, Keyword.UNDYING);
|
||||
|
||||
oppCreatM1 = CardLists.filter(oppCreatM1, input -> input.getNetToughness() <= 1 && input.canReceiveCounters(CounterEnumType.M1M1));
|
||||
oppCreatM1 = CardLists.filter(oppCreatM1, input -> input.getNetToughness() <= 1 && input.canReceiveCounters(CounterType.get(CounterEnumType.M1M1)));
|
||||
|
||||
Card best = ComputerUtilCard.getBestAI(oppCreatM1);
|
||||
if (best != null) {
|
||||
@@ -245,9 +247,10 @@ public class CountersPutAi extends CountersAi {
|
||||
} else if (sa.getSubAbility() != null
|
||||
&& "Self".equals(sa.getSubAbility().getParam("Defined"))
|
||||
&& sa.getSubAbility().getParamOrDefault("KW", "").contains("Hexproof")
|
||||
&& !source.getAbilityActivatedThisTurn().getActivators(sa).contains(ai)) {
|
||||
&& !AiCardMemory.isRememberedCard(ai, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
|
||||
// Bristling Hydra: save from death using a ping activation
|
||||
if (ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa).contains(source)) {
|
||||
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
} else if (ai.getCounters(CounterEnumType.ENERGY) > ComputerUtilCard.getMaxSAEnergyCostOnBattlefield(ai) + sa.getPayCosts().getCostEnergy().convertAmount()) {
|
||||
@@ -290,8 +293,10 @@ public class CountersPutAi extends CountersAi {
|
||||
|
||||
if (willActivate) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
|
||||
} else if (logic.equals("ChargeToBestCMC")) {
|
||||
return doChargeToCMCLogic(ai, sa);
|
||||
} else if (logic.equals("ChargeToBestOppControlledCMC")) {
|
||||
@@ -332,7 +337,7 @@ public class CountersPutAi extends CountersAi {
|
||||
Game game = ai.getGame();
|
||||
Combat combat = game.getCombat();
|
||||
|
||||
if (!source.canReceiveCounters(CounterEnumType.P1P1) || source.getCounters(CounterEnumType.P1P1) > 0) {
|
||||
if (!source.canReceiveCounters(CounterType.get(CounterEnumType.P1P1)) || source.getCounters(CounterEnumType.P1P1) > 0) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (combat != null && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
return doCombatAdaptLogic(source, amount, combat);
|
||||
@@ -344,7 +349,7 @@ public class CountersPutAi extends CountersAi {
|
||||
if (type.equals("P1P1")) {
|
||||
nPump = amount;
|
||||
}
|
||||
return FightAi.canFight(ai, sa, nPump, nPump);
|
||||
return FightAi.canFightAi(ai, sa, nPump, nPump);
|
||||
}
|
||||
|
||||
if (amountStr.equals("X")) {
|
||||
@@ -438,16 +443,17 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
sa.addDividedAllocation(c, amount);
|
||||
return decision;
|
||||
} else if (!hasSacCost) {
|
||||
// for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies
|
||||
return decision;
|
||||
} else {
|
||||
if (!hasSacCost) {
|
||||
// for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies
|
||||
return decision;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
|
||||
CardCollection list;
|
||||
if (sa.isCurse()) {
|
||||
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
|
||||
} else {
|
||||
@@ -603,21 +609,7 @@ public class CountersPutAi extends CountersAi {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||
}
|
||||
|
||||
final int currCounters = cards.get(0).getCounters(CounterType.getType(type));
|
||||
|
||||
// adding counters would cause counter amount to overflow
|
||||
if (Integer.MAX_VALUE - currCounters <= amount) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
if (type.equals("P1P1")) {
|
||||
if (Integer.MAX_VALUE - cards.get(0).getNetPower() <= amount) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
if (Integer.MAX_VALUE - cards.get(0).getNetToughness() <= amount) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
final int currCounters = cards.get(0).getCounters(CounterType.get(type));
|
||||
// each non +1/+1 counter on the card is a 10% chance of not
|
||||
// activating this ability.
|
||||
|
||||
@@ -632,7 +624,7 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
|
||||
// Useless since the card already has the keyword (or for another reason)
|
||||
if (ComputerUtil.isUselessCounter(CounterType.getType(type), cards.get(0))) {
|
||||
if (ComputerUtil.isUselessCounter(CounterType.get(type), cards.get(0))) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
@@ -679,12 +671,14 @@ public class CountersPutAi extends CountersAi {
|
||||
|| (sa.getRootAbility().isTrigger() && !sa.getRootAbility().isOptionalTrigger());
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
CardCollection list;
|
||||
CardCollection list = null;
|
||||
|
||||
if (sa.isCurse()) {
|
||||
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
|
||||
} else {
|
||||
list = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
|
||||
}
|
||||
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
|
||||
if (list.isEmpty() && isMandatoryTrigger) {
|
||||
@@ -700,8 +694,9 @@ public class CountersPutAi extends CountersAi {
|
||||
|| sa.getTargets().isEmpty()) {
|
||||
sa.resetTargets();
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (sa.isCurse()) {
|
||||
@@ -743,7 +738,10 @@ public class CountersPutAi extends CountersAi {
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final SpellAbility root = sa.getRootAbility();
|
||||
final Card source = sa.getHostCard();
|
||||
final String aiLogic = sa.getParam("AILogic");
|
||||
final String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
// boolean chance = true;
|
||||
boolean preferred = true;
|
||||
CardCollection list;
|
||||
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
|
||||
final boolean divided = sa.isDividedAsYouChoose();
|
||||
final int amount = AbilityUtils.calculateAmount(source, amountStr, sa);
|
||||
@@ -762,10 +760,28 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
|
||||
if ("ChargeToBestCMC".equals(aiLogic)) {
|
||||
if (mandatory) {
|
||||
AiAbilityDecision decision = doChargeToCMCLogic(ai, sa);
|
||||
if (decision.willingToPlay()) {
|
||||
// If the AI logic is to charge to best CMC, we can return true
|
||||
// if the logic was successfully applied or if it's mandatory.
|
||||
return decision;
|
||||
} else if (mandatory) {
|
||||
// If the logic was not applied and it's mandatory, we return false.
|
||||
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
|
||||
} else {
|
||||
// If the logic was not applied and it's not mandatory, we return false.
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else if ("ChargeToBestOppControlledCMC".equals(aiLogic)) {
|
||||
AiAbilityDecision decision = doChargeToOppCtrlCMCLogic(ai, sa);
|
||||
if (decision.willingToPlay()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else if (mandatory) {
|
||||
// If the logic was not applied and it's mandatory, we return false.
|
||||
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return doChargeToCMCLogic(ai, sa);
|
||||
}
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
@@ -789,6 +805,7 @@ public class CountersPutAi extends CountersAi {
|
||||
// things like Powder Keg, which are way too complex for the AI
|
||||
}
|
||||
} else if (sa.getTargetRestrictions().canOnlyTgtOpponent() && !sa.getTargetRestrictions().canTgtCreature()) {
|
||||
// can only target opponent
|
||||
PlayerCollection playerList = new PlayerCollection(IterableUtil.filter(
|
||||
sa.getTargetRestrictions().getAllCandidates(sa, true, true), Player.class));
|
||||
|
||||
@@ -803,32 +820,34 @@ public class CountersPutAi extends CountersAi {
|
||||
sa.getTargets().add(choice);
|
||||
}
|
||||
} else {
|
||||
if ("Fight".equals(aiLogic) || "PowerDmg".equals(aiLogic)) {
|
||||
String logic = sa.getParam("AILogic");
|
||||
if ("Fight".equals(logic) || "PowerDmg".equals(logic)) {
|
||||
int nPump = 0;
|
||||
if (type.equals("P1P1")) {
|
||||
nPump = amount;
|
||||
}
|
||||
AiAbilityDecision decision = FightAi.canFight(ai, sa, nPump, nPump);
|
||||
AiAbilityDecision decision = FightAi.canFightAi(ai, sa, nPump, nPump);
|
||||
if (decision.willingToPlay()) {
|
||||
return decision;
|
||||
}
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
|
||||
Iterable<Card> filteredField;
|
||||
if (sa.isCurse()) {
|
||||
filteredField = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
|
||||
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
|
||||
} else {
|
||||
filteredField = ai.getCardsIn(ZoneType.Battlefield);
|
||||
list = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
|
||||
}
|
||||
CardCollection list = CardLists.getTargetableCards(filteredField, sa);
|
||||
list = ComputerUtil.filterAITgts(sa, ai, list, false);
|
||||
int totalTargets = list.size();
|
||||
boolean preferred = true;
|
||||
list = CardLists.getTargetableCards(list, sa);
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, list, false);
|
||||
|
||||
int totalTargets = list.size();
|
||||
|
||||
sa.resetTargets();
|
||||
while (sa.canAddMoreTarget()) {
|
||||
if (mandatory) {
|
||||
// When things are mandatory, gotta handle a little differently
|
||||
if ((list.isEmpty() || !preferred) && sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
@@ -850,10 +869,12 @@ public class CountersPutAi extends CountersAi {
|
||||
if (list.isEmpty()) {
|
||||
// Not mandatory, or the the list was regenerated and is still empty,
|
||||
// so return whether or not we found enough targets
|
||||
return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
|
||||
if (sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
}
|
||||
|
||||
Card choice;
|
||||
Card choice = null;
|
||||
|
||||
// Choose targets here:
|
||||
if (sa.isCurse()) {
|
||||
@@ -862,27 +883,33 @@ public class CountersPutAi extends CountersAi {
|
||||
if (choice == null && mandatory) {
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
} else if (type.equals("M1M1")) {
|
||||
choice = ComputerUtilCard.getWorstCreatureAI(list);
|
||||
} else {
|
||||
choice = Aggregates.random(list);
|
||||
if (type.equals("M1M1")) {
|
||||
choice = ComputerUtilCard.getWorstCreatureAI(list);
|
||||
} else {
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
}
|
||||
} else if (preferred) {
|
||||
list = ComputerUtil.getSafeTargets(ai, sa, list);
|
||||
choice = chooseBoonTarget(list, type);
|
||||
if (choice == null && mandatory) {
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
} else if (type.equals("P1P1")) {
|
||||
choice = ComputerUtilCard.getWorstCreatureAI(list);
|
||||
} else {
|
||||
choice = Aggregates.random(list);
|
||||
if (preferred) {
|
||||
list = ComputerUtil.getSafeTargets(ai, sa, list);
|
||||
choice = chooseBoonTarget(list, type);
|
||||
if (choice == null && mandatory) {
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
} else {
|
||||
if (type.equals("P1P1")) {
|
||||
choice = ComputerUtilCard.getWorstCreatureAI(list);
|
||||
} else {
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (choice != null && divided) {
|
||||
int alloc = Math.max(amount / totalTargets, 1);
|
||||
if (sa.getTargets().size() == Math.min(totalTargets, sa.getMaxTargets()) - 1) {
|
||||
sa.addDividedAllocation(choice, left);
|
||||
} else {
|
||||
int alloc = Math.max(amount / totalTargets, 1);
|
||||
sa.addDividedAllocation(choice, alloc);
|
||||
left -= alloc;
|
||||
}
|
||||
@@ -952,8 +979,8 @@ public class CountersPutAi extends CountersAi {
|
||||
protected Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
|
||||
// Bolster does use this
|
||||
// TODO need more or less logic there?
|
||||
final CounterType m1m1 = CounterEnumType.M1M1;
|
||||
final CounterType p1p1 = CounterEnumType.P1P1;
|
||||
final CounterType m1m1 = CounterType.get(CounterEnumType.M1M1);
|
||||
final CounterType p1p1 = CounterType.get(CounterEnumType.P1P1);
|
||||
|
||||
// no logic if there is no options or no to choice
|
||||
if (!isOptional && Iterables.size(options) <= 1) {
|
||||
@@ -972,7 +999,9 @@ public class CountersPutAi extends CountersAi {
|
||||
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
|
||||
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
|
||||
|
||||
if (sa.isCurse()) {
|
||||
final boolean isCurse = sa.isCurse();
|
||||
|
||||
if (isCurse) {
|
||||
final CardCollection opponents = CardLists.filterControlledBy(options, ai.getOpponents());
|
||||
|
||||
if (!opponents.isEmpty()) {
|
||||
@@ -1069,10 +1098,11 @@ public class CountersPutAi extends CountersAi {
|
||||
Player ai = sa.getActivatingPlayer();
|
||||
GameEntity e = (GameEntity) params.get("Target");
|
||||
// for Card try to select not useless counter
|
||||
if (e instanceof Card c) {
|
||||
if (e instanceof Card) {
|
||||
Card c = (Card) e;
|
||||
if (c.getController().isOpponentOf(ai)) {
|
||||
if (options.contains(CounterEnumType.M1M1) && !c.hasKeyword(Keyword.UNDYING)) {
|
||||
return CounterEnumType.M1M1;
|
||||
if (options.contains(CounterType.get(CounterEnumType.M1M1)) && !c.hasKeyword(Keyword.UNDYING)) {
|
||||
return CounterType.get(CounterEnumType.M1M1);
|
||||
}
|
||||
for (CounterType type : options) {
|
||||
if (ComputerUtil.isNegativeCounter(type, c)) {
|
||||
@@ -1086,14 +1116,15 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (e instanceof Player p) {
|
||||
} else if (e instanceof Player) {
|
||||
Player p = (Player) e;
|
||||
if (p.isOpponentOf(ai)) {
|
||||
if (options.contains(CounterEnumType.POISON)) {
|
||||
return CounterEnumType.POISON;
|
||||
if (options.contains(CounterType.get(CounterEnumType.POISON))) {
|
||||
return CounterType.get(CounterEnumType.POISON);
|
||||
}
|
||||
} else {
|
||||
if (options.contains(CounterEnumType.EXPERIENCE)) {
|
||||
return CounterEnumType.EXPERIENCE;
|
||||
if (options.contains(CounterType.get(CounterEnumType.EXPERIENCE))) {
|
||||
return CounterType.get(CounterEnumType.EXPERIENCE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1197,9 +1228,12 @@ public class CountersPutAi extends CountersAi {
|
||||
}
|
||||
}
|
||||
if (numCtrs < optimalCMC) {
|
||||
// If the AI has less counters than the optimal CMC, it should play the ability.
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// If the AI has enough counters or more than the optimal CMC, it should not play the ability.
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
private AiAbilityDecision doChargeToOppCtrlCMCLogic(Player ai, SpellAbility sa) {
|
||||
@@ -1219,8 +1253,9 @@ public class CountersPutAi extends CountersAi {
|
||||
if (numCtrs < optimalCMC) {
|
||||
// If the AI has less counters than the optimal CMC, it should play the ability.
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// If the AI has enough counters or more than the optimal CMC, it should not play the ability.
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
// If the AI has enough counters or more than the optimal CMC, it should not play the ability.
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,18 +218,18 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
|
||||
Card tgt = (Card) params.get("Target");
|
||||
|
||||
// planeswalker has high priority for loyalty counters
|
||||
if (tgt.isPlaneswalker() && options.contains(CounterEnumType.LOYALTY)) {
|
||||
return CounterEnumType.LOYALTY;
|
||||
if (tgt.isPlaneswalker() && options.contains(CounterType.get(CounterEnumType.LOYALTY))) {
|
||||
return CounterType.get(CounterEnumType.LOYALTY);
|
||||
}
|
||||
|
||||
if (tgt.getController().isOpponentOf(ai)) {
|
||||
// creatures with BaseToughness below or equal zero might be
|
||||
// killed if their counters are removed
|
||||
if (tgt.isCreature() && tgt.getBaseToughness() <= 0) {
|
||||
if (options.contains(CounterEnumType.P1P1)) {
|
||||
return CounterEnumType.P1P1;
|
||||
} else if (options.contains(CounterEnumType.M1M1)) {
|
||||
return CounterEnumType.M1M1;
|
||||
if (options.contains(CounterType.get(CounterEnumType.P1P1))) {
|
||||
return CounterType.get(CounterEnumType.P1P1);
|
||||
} else if (options.contains(CounterType.get(CounterEnumType.M1M1))) {
|
||||
return CounterType.get(CounterEnumType.M1M1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,17 +241,17 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
|
||||
}
|
||||
} else {
|
||||
// this counters are treat first to be removed
|
||||
if ("Dark Depths".equals(tgt.getName()) && options.contains(CounterEnumType.ICE)) {
|
||||
if ("Dark Depths".equals(tgt.getName()) && options.contains(CounterType.get(CounterEnumType.ICE))) {
|
||||
CardCollectionView marit = ai.getCardsIn(ZoneType.Battlefield, "Marit Lage");
|
||||
boolean maritEmpty = marit.isEmpty() || Iterables.contains(marit, (Predicate<Card>) Card::ignoreLegendRule);
|
||||
|
||||
if (maritEmpty) {
|
||||
return CounterEnumType.ICE;
|
||||
return CounterType.get(CounterEnumType.ICE);
|
||||
}
|
||||
} else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterEnumType.P1P1)) {
|
||||
return CounterEnumType.P1P1;
|
||||
} else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterEnumType.M1M1)) {
|
||||
return CounterEnumType.M1M1;
|
||||
} else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterType.get(CounterEnumType.P1P1))) {
|
||||
return CounterType.get(CounterEnumType.P1P1);
|
||||
} else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterType.get(CounterEnumType.M1M1))) {
|
||||
return CounterType.get(CounterEnumType.M1M1);
|
||||
}
|
||||
|
||||
// fallback logic, select positive counter to add more
|
||||
|
||||
@@ -384,7 +384,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
if (targetCard.getController().isOpponentOf(ai)) {
|
||||
// if its a Planeswalker try to remove Loyality first
|
||||
if (targetCard.isPlaneswalker()) {
|
||||
return CounterEnumType.LOYALTY;
|
||||
return CounterType.get(CounterEnumType.LOYALTY);
|
||||
}
|
||||
for (CounterType type : options) {
|
||||
if (!ComputerUtil.isNegativeCounter(type, targetCard)) {
|
||||
@@ -392,10 +392,10 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (options.contains(CounterEnumType.M1M1) && targetCard.hasKeyword(Keyword.PERSIST)) {
|
||||
return CounterEnumType.M1M1;
|
||||
} else if (options.contains(CounterEnumType.P1P1) && targetCard.hasKeyword(Keyword.UNDYING)) {
|
||||
return CounterEnumType.P1P1;
|
||||
if (options.contains(CounterType.get(CounterEnumType.M1M1)) && targetCard.hasKeyword(Keyword.PERSIST)) {
|
||||
return CounterType.get(CounterEnumType.M1M1);
|
||||
} else if (options.contains(CounterType.get(CounterEnumType.P1P1)) && targetCard.hasKeyword(Keyword.UNDYING)) {
|
||||
return CounterType.get(CounterEnumType.P1P1);
|
||||
}
|
||||
for (CounterType type : options) {
|
||||
if (ComputerUtil.isNegativeCounter(type, targetCard)) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -20,6 +21,14 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
// based on what the expected targets could be
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
// abCost stuff that should probably be centralized...
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
if (abCost != null) {
|
||||
// AI currently disabled for some costs
|
||||
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
|
||||
}
|
||||
}
|
||||
// wait until stack is empty (prevents duplicate kills)
|
||||
if (!ai.getGame().getStack().isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.StackNotEmpty);
|
||||
@@ -133,9 +142,9 @@ public class DamageAllAi extends SpellAbilityAi {
|
||||
if (ComputerUtilCombat.predictDamageTo(opp, dmg, source, false) > 0) {
|
||||
// When using Pestilence to hurt players, do it at
|
||||
// the end of the opponent's turn only
|
||||
if (!"DmgAllCreaturesAndPlayers".equals(sa.getParam("AILogic"))
|
||||
|| (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)
|
||||
&& !ai.getGame().getPhaseHandler().isPlayerTurn(ai)))
|
||||
if ((!"DmgAllCreaturesAndPlayers".equals(sa.getParam("AILogic")))
|
||||
|| ((ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)
|
||||
&& (ai.getGame().getNonactivePlayers().contains(ai)))))
|
||||
// Need further improvement : if able to kill immediately with repeated activations, do not wait
|
||||
// for phases! Will also need to implement considering repeated activations for killed creatures!
|
||||
// || (ai.sa.getPayCosts(). ??? )
|
||||
|
||||
@@ -110,7 +110,15 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
|
||||
CardCollection list;
|
||||
|
||||
// Targeting
|
||||
if (sa.usesTargeting()) {
|
||||
// If there's X in payment costs and it's tied to targeting, make sure we set the XManaCostPaid first
|
||||
// (e.g. Heliod's Intervention)
|
||||
if ("X".equals(sa.getTargetRestrictions().getMinTargets()) && sa.getSVar("X").equals("Count$xPaid")) {
|
||||
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
|
||||
sa.getRootAbility().setXManaCostPaid(xPay);
|
||||
}
|
||||
|
||||
// Assume there where already enough targets chosen by AI Logic Above
|
||||
if (sa.hasParam("AILogic") && !sa.canAddMoreTarget() && sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
@@ -120,10 +128,7 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
sa.resetTargets();
|
||||
int maxTargets;
|
||||
|
||||
// If there's X in payment costs and it's tied to targeting, make sure we set the XManaCostPaid first
|
||||
// (e.g. Heliod's Intervention)
|
||||
if (sa.getRootAbility().costHasManaX() ||
|
||||
("X".equals(sa.getTargetRestrictions().getMinTargets()) && sa.getSVar("X").equals("Count$xPaid"))) {
|
||||
if (sa.getRootAbility().costHasManaX()) {
|
||||
// TODO: currently the AI will maximize mana spent on X, trying to maximize damage. This may need improvement.
|
||||
maxTargets = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
|
||||
// need to set XPaid to get the right number for
|
||||
|
||||
119
forge-ai/src/main/java/forge/ai/ability/DetainAi.java
Normal file
119
forge-ai/src/main/java/forge/ai/ability/DetainAi.java
Normal file
@@ -0,0 +1,119 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
public class DetainAi extends SpellAbilityAi {
|
||||
|
||||
Predicate<Card> CREATURE_OR_TAP_ABILITY = c -> {
|
||||
if (c.isCreature()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (final SpellAbility sa : c.getSpellAbilities()) {
|
||||
if (sa.isAbility() && sa.getPayCosts().hasTapCost()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
protected boolean prefTargeting(final Player ai, final Card source, final SpellAbility sa, final boolean mandatory) {
|
||||
final Game game = ai.getGame();
|
||||
CardCollection list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
|
||||
list = CardLists.filter(list, CREATURE_OR_TAP_ABILITY);
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, list, true);
|
||||
|
||||
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
while (sa.canAddMoreTarget()) {
|
||||
Card choice = null;
|
||||
if (list.isEmpty()) {
|
||||
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
|
||||
if (!mandatory) {
|
||||
sa.resetTargets();
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
PhaseHandler phase = game.getPhaseHandler();
|
||||
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
Card primeTarget = ComputerUtil.getKilledByTargeting(sa, list);
|
||||
if (primeTarget != null) {
|
||||
choice = primeTarget;
|
||||
} else if (phase.isPlayerTurn(ai) && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
// Tap creatures possible blockers before combat during AI's turn.
|
||||
List<Card> attackers;
|
||||
if (phase.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
//Combat has already started
|
||||
attackers = game.getCombat().getAttackers();
|
||||
} else {
|
||||
attackers = CardLists.filter(ai.getCreaturesInPlay(), c -> CombatUtil.canAttack(c, opp));
|
||||
attackers.remove(source);
|
||||
}
|
||||
List<Card> creatureList = CardLists.filter(list, CardPredicates.possibleBlockerForAtLeastOne(attackers));
|
||||
|
||||
// TODO check if own creature would be forced to attack and we want to keep it alive
|
||||
|
||||
if (!attackers.isEmpty() && !creatureList.isEmpty()) {
|
||||
choice = ComputerUtilCard.getBestCreatureAI(creatureList);
|
||||
} else if (sa.isTrigger() || ComputerUtil.castSpellInMain1(ai, sa)) {
|
||||
choice = ComputerUtilCard.getMostExpensivePermanentAI(list);
|
||||
}
|
||||
} else if (phase.isPlayerTurn(opp)
|
||||
&& phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
// Tap creatures possible blockers before combat during AI's turn.
|
||||
if (list.anyMatch(CardPredicates.CREATURES)) {
|
||||
List<Card> creatureList = CardLists.filter(list, c -> c.isCreature() && CombatUtil.canAttack(c, opp));
|
||||
choice = ComputerUtilCard.getBestCreatureAI(creatureList);
|
||||
} else { // no creatures available
|
||||
choice = ComputerUtilCard.getMostExpensivePermanentAI(list);
|
||||
}
|
||||
} else {
|
||||
choice = ComputerUtilCard.getMostExpensivePermanentAI(list);
|
||||
}
|
||||
|
||||
if (choice == null) { // can't find anything left
|
||||
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
|
||||
if (!mandatory) {
|
||||
sa.resetTargets();
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
if (!ComputerUtil.shouldCastLessThanMax(ai, source)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
list.remove(choice);
|
||||
sa.getTargets().add(choice);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CounterEnumType;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.cost.*;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
@@ -369,7 +370,7 @@ public class DrawAi extends SpellAbilityAi {
|
||||
|
||||
// try to make opponent lose to poison
|
||||
// currently only Caress of Phyrexia
|
||||
if (getPoison != null && oppA.canReceiveCounters(CounterEnumType.POISON)) {
|
||||
if (getPoison != null && oppA.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
|
||||
if (oppA.getPoisonCounters() + numCards > 9) {
|
||||
sa.getTargets().add(oppA);
|
||||
return true;
|
||||
@@ -413,7 +414,7 @@ public class DrawAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
if (getPoison != null && ai.canReceiveCounters(CounterEnumType.POISON)) {
|
||||
if (getPoison != null && ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
|
||||
if (numCards + ai.getPoisonCounters() >= 8) {
|
||||
aiTarget = false;
|
||||
}
|
||||
@@ -471,7 +472,7 @@ public class DrawAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// ally would lose because of poison
|
||||
if (getPoison != null && ally.canReceiveCounters(CounterEnumType.POISON) && ally.getPoisonCounters() + numCards > 9) {
|
||||
if (getPoison != null && ally.canReceiveCounters(CounterType.get(CounterEnumType.POISON)) && ally.getPoisonCounters() + numCards > 9) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,10 @@ import forge.game.replacement.ReplacementType;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityStackInstance;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.staticability.StaticAbilityMode;
|
||||
import forge.game.zone.MagicStack;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.FileSection;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.TextUtil;
|
||||
import forge.util.collect.FCollectionView;
|
||||
@@ -37,17 +39,22 @@ public class EffectAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
final Game game = ai.getGame();
|
||||
final PhaseHandler phase = game.getPhaseHandler();
|
||||
boolean randomReturn = MyRandom.getRandom().nextFloat() <= .6667;
|
||||
String logic = "";
|
||||
|
||||
if (sa.hasParam("AILogic")) {
|
||||
logic = sa.getParam("AILogic");
|
||||
final PhaseHandler phase = game.getPhaseHandler();
|
||||
if (logic.equals("BeginningOfOppTurn")) {
|
||||
if (!phase.getPlayerTurn().isOpponentOf(ai) || phase.getPhase().isAfter(PhaseType.DRAW)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
randomReturn = true;
|
||||
} else if (logic.equals("EndOfOppTurn")) {
|
||||
if (!phase.getPlayerTurn().isOpponentOf(ai) || phase.getPhase().isBefore(PhaseType.END_OF_TURN)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
randomReturn = true;
|
||||
} else if (logic.equals("KeepOppCreatsLandsTapped")) {
|
||||
for (Player opp : ai.getOpponents()) {
|
||||
boolean worthHolding = false;
|
||||
@@ -270,7 +277,7 @@ public class EffectAi extends SpellAbilityAi {
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (logic.equals("Fight")) {
|
||||
return FightAi.canFight(ai, sa, 0,0);
|
||||
return FightAi.canFightAi(ai, sa, 0,0);
|
||||
} else if (logic.equals("Pump")) {
|
||||
sa.resetTargets();
|
||||
List<Card> options = CardUtil.getValidCardsToTarget(sa);
|
||||
@@ -289,7 +296,6 @@ public class EffectAi extends SpellAbilityAi {
|
||||
} else if (logic.equals("YawgmothsWill")) {
|
||||
return SpecialCardAi.YawgmothsWill.consider(ai, sa) ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (logic.startsWith("NeedCreatures")) {
|
||||
// TODO convert to AiCheckSVar
|
||||
if (ai.getCreaturesInPlay().isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
@@ -324,8 +330,8 @@ public class EffectAi extends SpellAbilityAi {
|
||||
Combat combat = game.getCombat();
|
||||
if (combat != null && combat.isAttacking(host, ai) && !combat.isBlocked(host)
|
||||
&& phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
&& !host.getAbilityActivatedThisTurn().getActivators(sa).contains(ai)) {
|
||||
// ideally needs once per combat or something
|
||||
&& !AiCardMemory.isRememberedCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
|
||||
AiCardMemory.rememberCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN); // ideally needs once per combat or something
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
@@ -379,6 +385,118 @@ public class EffectAi extends SpellAbilityAi {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
} else if (sa.hasParam("RememberObjects")) { //generic
|
||||
boolean cantAttack = false;
|
||||
boolean cantBlock = false;
|
||||
boolean cantActivate = false;
|
||||
|
||||
String duraction = sa.getParam("Duration");
|
||||
|
||||
String matchStr = "Card.IsRemembered";
|
||||
|
||||
for (String st : sa.getParam("StaticAbilities").split(",")) {
|
||||
Map<String, String> params = FileSection.parseToMap(sa.getSVar(st), FileSection.DOLLAR_SIGN_KV_SEPARATOR);
|
||||
List<StaticAbilityMode> modes = StaticAbilityMode.listValueOf(params.get("Mode"));
|
||||
|
||||
if (modes.contains(StaticAbilityMode.CantAttack) && matchStr.equals(params.get("ValidCard"))) {
|
||||
cantAttack = true;
|
||||
}
|
||||
if (modes.contains(StaticAbilityMode.CantBlock) && matchStr.equals(params.get("ValidCard"))) {
|
||||
cantBlock = true;
|
||||
}
|
||||
if (modes.contains(StaticAbilityMode.CantBlockBy) && matchStr.equals(params.get("ValidBlocker"))) {
|
||||
cantBlock = true;
|
||||
}
|
||||
if (modes.contains(StaticAbilityMode.CantBeActivated) && matchStr.equals(params.get("ValidCard"))) {
|
||||
cantActivate = true;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO add more cases later
|
||||
if (!cantAttack && !cantBlock && !cantActivate) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if (cantBlock && duraction == null && phase.isPlayerTurn(ai) && !phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
|
||||
CardCollection list = new CardCollection(CardUtil.getValidCardsToTarget(sa));
|
||||
|
||||
list = ComputerUtil.filterAITgts(sa, ai, list, true);
|
||||
|
||||
if (list.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
List<Card> oppCreatures = CardLists.filterAsList(list, c -> {
|
||||
return c.isCreature() && c.getController().isOpponentOf(ai);
|
||||
});
|
||||
|
||||
List<Card> oppWithAbilities = CardLists.filterAsList(list, c -> {
|
||||
return !c.isCreature() && c.getController().isOpponentOf(ai) && c.getSpellAbilities().anyMatch(SpellAbility::isActivatedAbility);
|
||||
});
|
||||
|
||||
if (cantAttack || cantBlock) {
|
||||
if (oppCreatures.isEmpty()) {
|
||||
if (!cantActivate || oppWithAbilities.isEmpty()) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
while (sa.canAddMoreTarget()) {
|
||||
Card choice = null;
|
||||
if (cantAttack && cantBlock && !oppCreatures.isEmpty()) {
|
||||
Card primeTarget = ComputerUtil.getKilledByTargeting(sa, oppCreatures);
|
||||
if (primeTarget != null) {
|
||||
choice = primeTarget;
|
||||
} else if (phase.isPlayerTurn(ai) && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
// Tap creatures possible blockers before combat during AI's turn.
|
||||
List<Card> attackers;
|
||||
if (phase.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
//Combat has already started
|
||||
attackers = game.getCombat().getAttackers();
|
||||
} else {
|
||||
attackers = CardLists.filter(ai.getCreaturesInPlay(), c -> CombatUtil.canAttack(c, opp));
|
||||
}
|
||||
List<Card> creatureList = CardLists.filter(list, CardPredicates.possibleBlockerForAtLeastOne(attackers));
|
||||
|
||||
// TODO check if own creature would be forced to attack and we want to keep it alive
|
||||
|
||||
if (!attackers.isEmpty() && !creatureList.isEmpty()) {
|
||||
choice = ComputerUtilCard.getBestCreatureAI(creatureList);
|
||||
} else if (sa.isTrigger() || ComputerUtil.castSpellInMain1(ai, sa)) {
|
||||
choice = ComputerUtilCard.getMostExpensivePermanentAI(list);
|
||||
}
|
||||
}
|
||||
} // TODO add logic to tap non creatures with activated abilities if cantActivate is true
|
||||
|
||||
if (choice == null) { // can't find anything left
|
||||
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
|
||||
sa.resetTargets();
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else {
|
||||
if (!ComputerUtil.shouldCastLessThanMax(ai, sa.getHostCard())) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
list.remove(choice);
|
||||
oppCreatures.remove(choice);
|
||||
sa.getTargets().add(choice);
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else { //no AILogic
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import forge.ai.*;
|
||||
import forge.game.Game;
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.Game;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.token.TokenInfo;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.cost.CostPayLife;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -34,26 +36,6 @@ public class EndureAi extends SpellAbilityAi {
|
||||
sa.getTargets().add(bestCreature);
|
||||
}
|
||||
|
||||
// Card-specific logic
|
||||
final String num = sa.getParamOrDefault("Num", "1");
|
||||
if ("X".equals(num) && sa.getPayCosts().hasSpecificCostType(CostPayLife.class)) {
|
||||
if (!aiPlayer.getGame().getPhaseHandler().is(PhaseType.MAIN2)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||
}
|
||||
int curLife = aiPlayer.getLife();
|
||||
int dangerLife = (((PlayerControllerAi) aiPlayer.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD));
|
||||
if (curLife <= dangerLife) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
|
||||
}
|
||||
int availableMana = ComputerUtilMana.getAvailableManaEstimate(aiPlayer) - 1;
|
||||
int maxEndureX = Math.min(availableMana, curLife - dangerLife);
|
||||
if (maxEndureX > 0) {
|
||||
sa.setXManaCostPaid(maxEndureX);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
|
||||
}
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ public class FightAi extends SpellAbilityAi {
|
||||
* @param power bonus to power
|
||||
* @return true if fight effect should be played, false otherwise
|
||||
*/
|
||||
public static AiAbilityDecision canFight(final Player ai, final SpellAbility sa, int power, int toughness) {
|
||||
public static AiAbilityDecision canFightAi(final Player ai, final SpellAbility sa, int power, int toughness) {
|
||||
final Card source = sa.getHostCard();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
AbilitySub tgtFight = sa.getSubAbility();
|
||||
|
||||
@@ -12,13 +12,15 @@ import forge.game.spellability.SpellAbility;
|
||||
public class FlipACoinAi extends SpellAbilityAi {
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#checkApiLogic(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
if (sa.hasParam("AILogic")) {
|
||||
String ailogic = sa.getParam("AILogic");
|
||||
if (ailogic.equals("PhaseOut")) {
|
||||
if (ailogic.equals("Never")) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
} else if (ailogic.equals("PhaseOut")) {
|
||||
if (!ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa).contains(sa.getHostCard())) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
|
||||
import forge.ai.AiAbilityDecision;
|
||||
import forge.ai.AiPlayDecision;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
|
||||
@@ -76,8 +76,7 @@ public class ManaAi extends SpellAbilityAi {
|
||||
return ph.is(PhaseType.MAIN2, ai) || ph.is(PhaseType.MAIN1, ai);
|
||||
}
|
||||
if ("AtOppEOT".equals(logic)) {
|
||||
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai
|
||||
&& (!ai.getManaPool().hasBurn() || !ai.canLoseLife() || ai.cantLoseForZeroOrLessLife());
|
||||
return (!ai.getManaPool().hasBurn() || !ai.canLoseLife() || ai.cantLoseForZeroOrLessLife()) && ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai;
|
||||
}
|
||||
return super.checkPhaseRestrictions(ai, sa, ph, logic);
|
||||
}
|
||||
@@ -159,7 +158,7 @@ public class ManaAi extends SpellAbilityAi {
|
||||
int numCounters = 0;
|
||||
int manaSurplus = 0;
|
||||
if ("Count$xPaid".equals(host.getSVar("X")) && sa.getPayCosts().hasSpecificCostType(CostRemoveCounter.class)) {
|
||||
CounterType ctrType = CounterEnumType.KI; // Petalmane Baku
|
||||
CounterType ctrType = CounterType.get(CounterEnumType.KI); // Petalmane Baku
|
||||
for (CostPart part : sa.getPayCosts().getCostParts()) {
|
||||
if (part instanceof CostRemoveCounter) {
|
||||
ctrType = ((CostRemoveCounter)part).counter;
|
||||
@@ -278,4 +277,10 @@ public class ManaAi extends SpellAbilityAi {
|
||||
}
|
||||
return !lose;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
|
||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||
return checkApiLogic(ai, sa);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,10 @@ public class MeldAi extends SpellAbilityAi {
|
||||
boolean hasPrimaryMeld = cardsOTB.anyMatch(CardPredicates.nameEquals(primaryMeld).and(CardPredicates.isOwner(aiPlayer)));
|
||||
boolean hasSecondaryMeld = cardsOTB.anyMatch(CardPredicates.nameEquals(secondaryMeld).and(CardPredicates.isOwner(aiPlayer)));
|
||||
if (hasPrimaryMeld && hasSecondaryMeld && sa.getHostCard().getName().equals(primaryMeld)) {
|
||||
// If the primary meld card is on the battlefield and both meld cards are owned by the AI, play the ability
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
// If the secondary meld card is on the battlefield and it is the one being activated, play the ability
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,14 @@ public class MillAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
|
||||
if (aiLogic.equals("LilianaMill")) {
|
||||
// TODO convert to AICheckSVar
|
||||
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
|
||||
if (aiLogic.equals("Main1")) {
|
||||
return !ph.getPhase().isBefore(PhaseType.MAIN2) || sa.hasParam("ActivationPhases")
|
||||
|| ComputerUtil.castSpellInMain1(ai, sa);
|
||||
} else if (aiLogic.equals("EndOfOppTurn")) {
|
||||
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai);
|
||||
} else if (aiLogic.equals("LilianaMill")) {
|
||||
// Only mill if a "Raise Dead" target is available, in case of control decks with few creatures
|
||||
return CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES).size() >= 1;
|
||||
}
|
||||
@@ -50,10 +56,9 @@ public class MillAi extends SpellAbilityAi {
|
||||
// because they are also potentially useful for combat
|
||||
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai);
|
||||
}
|
||||
return !ph.getPhase().isBefore(PhaseType.MAIN2) || sa.hasParam("ActivationPhases")
|
||||
|| ComputerUtil.castSpellInMain1(ai, sa);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
/*
|
||||
|
||||
@@ -6,10 +6,10 @@ import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
|
||||
@@ -27,8 +27,7 @@ public class MustBlockAi extends SpellAbilityAi {
|
||||
|
||||
if (combat == null || !combat.isAttacking(source)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
if (source.getAbilityActivatedThisTurn().getActivators(sa).contains(aiPlayer)) {
|
||||
} else if (AiCardMemory.isRememberedCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
|
||||
// The AI can meaningfully do it only to one creature per card yet, trying to do it to multiple cards
|
||||
// may result in overextending and losing the attacker
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
@@ -38,7 +37,11 @@ public class MustBlockAi extends SpellAbilityAi {
|
||||
|
||||
if (!list.isEmpty()) {
|
||||
final Card blocker = ComputerUtilCard.getBestCreatureAI(list);
|
||||
if (blocker == null) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
sa.getTargets().add(blocker);
|
||||
AiCardMemory.rememberCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@@ -60,6 +63,11 @@ public class MustBlockAi extends SpellAbilityAi {
|
||||
protected AiAbilityDecision doTriggerNoCost(final Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
// only use on creatures that can attack
|
||||
if (!ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
Card attacker = source;
|
||||
if (sa.hasParam("DefinedAttacker")) {
|
||||
final List<Card> cards = AbilityUtils.getDefinedCards(source, sa.getParam("DefinedAttacker"), sa);
|
||||
@@ -73,9 +81,13 @@ public class MustBlockAi extends SpellAbilityAi {
|
||||
boolean chance = false;
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
List<Card> list = determineGoodBlockers(attacker, ai, ai.getWeakestOpponent(), sa, true, true);
|
||||
if (list.isEmpty() && mandatory) {
|
||||
list = CardUtil.getValidCardsToTarget(sa);
|
||||
final List<Card> list = determineGoodBlockers(attacker, ai, ai.getWeakestOpponent(), sa, true, true);
|
||||
if (list.isEmpty()) {
|
||||
if (sa.isTargetNumberValid()) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
}
|
||||
final Card blocker = ComputerUtilCard.getBestCreatureAI(list);
|
||||
if (blocker == null) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package forge.ai.ability;
|
||||
import forge.ai.*;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
@@ -28,6 +29,11 @@ public class PeekAndRevealAi extends SpellAbilityAi {
|
||||
if (aiPlayer.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} else if ("EndOfOppTurn".equals(logic)) {
|
||||
PhaseHandler ph = aiPlayer.getGame().getPhaseHandler();
|
||||
if (!(ph.getNextTurn() == aiPlayer && ph.is(PhaseType.END_OF_TURN))) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
// So far this only appears on Triggers, but will expand
|
||||
// once things get converted from Dig + NoMove
|
||||
|
||||
@@ -78,7 +78,7 @@ public class PermanentCreatureAi extends PermanentAi {
|
||||
|| ph.getPhase().isBefore(PhaseType.END_OF_TURN))
|
||||
&& ai.getManaPool().totalMana() <= 0
|
||||
&& (ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS))
|
||||
&& !card.hasETBTrigger(true) && !card.hasSVar("AmbushAI")
|
||||
&& (!card.hasETBTrigger(true) && !card.hasSVar("AmbushAI"))
|
||||
&& game.getStack().isEmpty()
|
||||
&& !ComputerUtil.castPermanentInMain1(ai, sa)) {
|
||||
// AiPlayDecision.AnotherTime;
|
||||
|
||||
@@ -33,8 +33,10 @@ public class PhasesAi extends SpellAbilityAi {
|
||||
final boolean isThreatened = ComputerUtil.predictThreatenedObjects(aiPlayer, null, true).contains(source);
|
||||
if (isThreatened) {
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
|
||||
@@ -153,8 +153,8 @@ public class PlayAi extends SpellAbilityAi {
|
||||
final boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
|
||||
final CardStateName state;
|
||||
if (sa.hasParam("CastTransformed")) {
|
||||
state = CardStateName.Backside;
|
||||
options.forEach(c -> c.changeToState(CardStateName.Backside));
|
||||
state = CardStateName.Transformed;
|
||||
options.forEach(c -> c.changeToState(CardStateName.Transformed));
|
||||
} else {
|
||||
state = CardStateName.Original;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import forge.ai.ComputerUtil;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.CounterEnumType;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.GameLossReason;
|
||||
@@ -64,7 +65,7 @@ public class PoisonAi extends SpellAbilityAi {
|
||||
boolean result;
|
||||
if (sa.usesTargeting()) {
|
||||
result = tgtPlayer(ai, sa, mandatory);
|
||||
} else if (mandatory || !ai.canReceiveCounters(CounterEnumType.POISON)) {
|
||||
} else if (mandatory || !ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
|
||||
// mandatory or ai is uneffected
|
||||
result = true;
|
||||
} else {
|
||||
@@ -89,7 +90,7 @@ public class PoisonAi extends SpellAbilityAi {
|
||||
PlayerCollection betterTgts = tgts.filter(input -> {
|
||||
if (input.cantLoseCheck(GameLossReason.Poisoned)) {
|
||||
return false;
|
||||
} else if (!input.canReceiveCounters(CounterEnumType.POISON)) {
|
||||
} else if (!input.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -108,7 +109,7 @@ public class PoisonAi extends SpellAbilityAi {
|
||||
if (tgts.isEmpty()) {
|
||||
if (mandatory) {
|
||||
// AI is uneffected
|
||||
if (ai.canBeTargetedBy(sa) && !ai.canReceiveCounters(CounterEnumType.POISON)) {
|
||||
if (ai.canBeTargetedBy(sa) && !ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
|
||||
sa.getTargets().add(ai);
|
||||
return true;
|
||||
}
|
||||
@@ -120,7 +121,7 @@ public class PoisonAi extends SpellAbilityAi {
|
||||
if (input.cantLoseCheck(GameLossReason.Poisoned)) {
|
||||
return true;
|
||||
}
|
||||
return !input.canReceiveCounters(CounterEnumType.POISON);
|
||||
return !input.canReceiveCounters(CounterType.get(CounterEnumType.POISON));
|
||||
});
|
||||
if (!betterAllies.isEmpty()) {
|
||||
allies = betterAllies;
|
||||
|
||||
@@ -7,6 +7,8 @@ import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.*;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostTapType;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
@@ -21,6 +23,13 @@ import java.util.*;
|
||||
|
||||
public class PumpAi extends PumpAiBase {
|
||||
|
||||
private static boolean hasTapCost(final Cost cost, final Card source) {
|
||||
if (cost == null) {
|
||||
return true;
|
||||
}
|
||||
return cost.hasSpecificCostType(CostTapType.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
|
||||
if ("MoveCounter".equals(aiLogic)) {
|
||||
@@ -87,7 +96,7 @@ public class PumpAi extends PumpAiBase {
|
||||
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
|
||||
final Game game = ai.getGame();
|
||||
boolean main1Preferred = "Main1IfAble".equals(sa.getParam("AILogic")) && ph.is(PhaseType.MAIN1, ai);
|
||||
if (game.getStack().isEmpty() && sa.getPayCosts().hasTapCost()) {
|
||||
if (game.getStack().isEmpty() && hasTapCost(sa.getPayCosts(), sa.getHostCard())) {
|
||||
if (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && ph.isPlayerTurn(ai)) {
|
||||
return false;
|
||||
}
|
||||
@@ -338,13 +347,13 @@ public class PumpAi extends PumpAiBase {
|
||||
}
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
|
||||
//Targeted
|
||||
if (!pumpTgtAI(ai, sa, defense, attack, false, false)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
} // pumpPlayAI()
|
||||
|
||||
private boolean pumpTgtAI(final Player ai, final SpellAbility sa, final int defense, final int attack, final boolean mandatory,
|
||||
boolean immediately) {
|
||||
@@ -453,7 +462,7 @@ public class PumpAi extends PumpAiBase {
|
||||
}
|
||||
|
||||
if (isFight) {
|
||||
return FightAi.canFight(ai, sa, attack, defense).willingToPlay();
|
||||
return FightAi.canFightAi(ai, sa, attack, defense).willingToPlay();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,28 +487,18 @@ public class PumpAi extends PumpAiBase {
|
||||
}
|
||||
}
|
||||
|
||||
if (game.getStack().isEmpty() && sa.getPayCosts().hasTapCost()) {
|
||||
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
||||
&& game.getPhaseHandler().isPlayerTurn(ai)) {
|
||||
list.remove(source);
|
||||
}
|
||||
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
&& game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai)) {
|
||||
list.remove(source);
|
||||
}
|
||||
}
|
||||
|
||||
// Detain target nonland permanent: don't target noncreature permanents that don't have
|
||||
// any activated abilities.
|
||||
if ("DetainNonLand".equals(sa.getParam("AILogic"))) {
|
||||
list = CardLists.filter(list, CardPredicates.CREATURES.or(card -> {
|
||||
for (SpellAbility sa1 : card.getSpellAbilities()) {
|
||||
if (sa1.isActivatedAbility()) {
|
||||
return true;
|
||||
}
|
||||
if (game.getStack().isEmpty()) {
|
||||
// If the cost is tapping, don't activate before declare attack/block
|
||||
if (sa.getPayCosts().hasTapCost()) {
|
||||
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
||||
&& game.getPhaseHandler().isPlayerTurn(ai)) {
|
||||
list.remove(sa.getHostCard());
|
||||
}
|
||||
return false;
|
||||
}));
|
||||
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
&& game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai)) {
|
||||
list.remove(sa.getHostCard());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
|
||||
@@ -147,7 +147,7 @@ public class PumpAllAi extends PumpAiBase {
|
||||
|
||||
// important to call canPlay first so targets are added if needed
|
||||
AiAbilityDecision decision = canPlay(ai, sa);
|
||||
if (mandatory && !decision.decision().willingToPlay()) {
|
||||
if (mandatory && decision.decision().willingToPlay()) {
|
||||
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
|
||||
}
|
||||
return decision;
|
||||
|
||||
@@ -37,12 +37,13 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// Do it once per turn, generally (may be improved later)
|
||||
if (source.getAbilityActivatedThisTurn().getActivators(sa).contains(aiPlayer)) {
|
||||
if (AiCardMemory.isRememberedCardByName(aiPlayer, source.getName(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
// ability is targeted
|
||||
sa.resetTargets();
|
||||
|
||||
PlayerCollection targetableOpps = aiPlayer.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
|
||||
@@ -61,6 +62,14 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
|
||||
} else {
|
||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); // could not find a valid target
|
||||
}
|
||||
|
||||
if (!canTgtHuman || !canTgtAI) {
|
||||
// can't target another player anyway, remember for no second activation this turn
|
||||
AiCardMemory.rememberCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
|
||||
}
|
||||
} else {
|
||||
// if it's just defined, no big deal
|
||||
AiCardMemory.rememberCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
|
||||
}
|
||||
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
@@ -71,6 +80,8 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
// Specific details of ordering cards are handled by PlayerControllerAi#orderMoveToZoneList
|
||||
|
||||
AiAbilityDecision decision = canPlay(ai, sa);
|
||||
if (decision.willingToPlay()) {
|
||||
return decision;
|
||||
|
||||
@@ -88,9 +88,9 @@ public class ScryAi extends SpellAbilityAi {
|
||||
// and right before the beginning of AI's turn, if possible, to avoid mana locking the AI and also to
|
||||
// try to scry right before drawing a card. Also, avoid tapping creatures in the AI's turn, if possible,
|
||||
// even if there's no mana cost.
|
||||
if (sa.getPayCosts().hasTapCost()
|
||||
&& (sa.getPayCosts().hasManaCost() || sa.getHostCard().isCreature())
|
||||
&& !isSorcerySpeed(sa, ai)) {
|
||||
if (logic.equals("AtOppEOT") || (sa.getPayCosts().hasTapCost()
|
||||
&& (sa.getPayCosts().hasManaCost() || (sa.getHostCard() != null && sa.getHostCard().isCreature()))
|
||||
&& !isSorcerySpeed(sa, ai))) {
|
||||
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@ public class SurveilAi extends SpellAbilityAi {
|
||||
*/
|
||||
@Override
|
||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
if (sa.usesTargeting()) { // TODO: It doesn't appear that Surveil ever targets, is this necessary?
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(ai);
|
||||
}
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
@@ -88,6 +92,7 @@ public class SurveilAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (randomReturn) {
|
||||
AiCardMemory.rememberCard(ai, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
|
||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,19 @@ import java.util.function.Predicate;
|
||||
|
||||
public abstract class TapAiBase extends SpellAbilityAi {
|
||||
|
||||
Predicate<Card> CREATURE_OR_TAP_ABILITY = c -> {
|
||||
if (c.isCreature()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (final SpellAbility sa : c.getSpellAbilities()) {
|
||||
if (sa.isAbility() && sa.getPayCosts().hasTapCost()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* tapTargetList.
|
||||
@@ -102,34 +115,12 @@ public abstract class TapAiBase extends SpellAbilityAi {
|
||||
final Game game = ai.getGame();
|
||||
CardCollection tapList = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
|
||||
tapList = CardLists.filter(tapList, CardPredicates.CAN_TAP);
|
||||
tapList = CardLists.filter(tapList, c -> {
|
||||
if (c.isCreature()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (final SpellAbility sa1 : c.getSpellAbilities()) {
|
||||
if (sa1.isAbility() && sa1.getPayCosts().hasTapCost()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
tapList = CardLists.filter(tapList, CREATURE_OR_TAP_ABILITY);
|
||||
|
||||
//use broader approach when the cost is a positive thing
|
||||
if (tapList.isEmpty() && ComputerUtil.activateForCost(sa, ai)) {
|
||||
tapList = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
|
||||
tapList = CardLists.filter(tapList, c -> {
|
||||
if (c.isCreature()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (final SpellAbility sa12 : c.getSpellAbilities()) {
|
||||
if (sa12.isAbility() && sa12.getPayCosts().hasTapCost()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
tapList = CardLists.filter(tapList, CREATURE_OR_TAP_ABILITY);
|
||||
}
|
||||
|
||||
//try to exclude things that will already be tapped due to something on stack or because something is
|
||||
|
||||
@@ -8,6 +8,7 @@ import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CounterEnumType;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.player.PlayerController;
|
||||
@@ -39,7 +40,7 @@ public class TimeTravelAi extends SpellAbilityAi {
|
||||
// so removing them is good; stuff on the battlefield is usually stuff like Vanishing or As Foretold, which favors adding Time
|
||||
// counters for better effect, but exceptions should be added here).
|
||||
Card target = (Card)params.get("Target");
|
||||
return !ComputerUtil.isNegativeCounter(CounterEnumType.TIME, target);
|
||||
return !ComputerUtil.isNegativeCounter(CounterType.get(CounterEnumType.TIME), target);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -26,6 +26,7 @@ import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.player.PlayerCollection;
|
||||
import forge.game.player.PlayerPredicates;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
@@ -66,15 +67,17 @@ public class TokenAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
}
|
||||
String tokenAmount = sa.getParamOrDefault("TokenAmount", "1");
|
||||
|
||||
Card actualToken = spawnToken(ai, sa);
|
||||
|
||||
if (actualToken == null || (actualToken.isCreature() && actualToken.getNetToughness() < 1)) {
|
||||
// planeswalker plus ability or sub-ability is useful
|
||||
return pwPlus || sa.getSubAbility() != null;
|
||||
final AbilitySub sub = sa.getSubAbility();
|
||||
// useful
|
||||
// no token created
|
||||
return pwPlus || (sub != null && SpellApiToAi.Converter.get(sub).chkDrawback(sub, ai).willingToPlay()); // planeswalker plus ability or sub-ability is
|
||||
}
|
||||
|
||||
String tokenAmount = sa.getParamOrDefault("TokenAmount", "1");
|
||||
String tokenPower = sa.getParamOrDefault("TokenPower", actualToken.getBasePowerString());
|
||||
String tokenToughness = sa.getParamOrDefault("TokenToughness", actualToken.getBaseToughnessString());
|
||||
|
||||
@@ -131,6 +134,9 @@ public class TokenAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
/*
|
||||
* readParameters() is called in checkPhaseRestrictions
|
||||
*/
|
||||
final Game game = ai.getGame();
|
||||
final Player opp = ai.getWeakestOpponent();
|
||||
|
||||
|
||||
@@ -31,8 +31,6 @@ public final class ImageKeys {
|
||||
public static final String MONARCH_IMAGE = "monarch";
|
||||
public static final String THE_RING_IMAGE = "the_ring";
|
||||
public static final String RADIATION_IMAGE = "radiation";
|
||||
public static final String SPEED_IMAGE = "speed";
|
||||
public static final String MAX_SPEED_IMAGE = "max_speed";
|
||||
|
||||
public static final String BACKFACE_POSTFIX = "$alt";
|
||||
public static final String SPECFACE_W = "$wspec";
|
||||
|
||||
@@ -18,9 +18,9 @@ import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
/**
|
||||
* The class holding game invariants, such as cards, editions, game formats. All that data, which is not supposed to be changed by player
|
||||
*
|
||||
@@ -29,6 +29,8 @@ import java.util.stream.Collectors;
|
||||
public class StaticData {
|
||||
private final CardStorageReader cardReader;
|
||||
private final CardStorageReader tokenReader;
|
||||
private final CardStorageReader customCardReader;
|
||||
|
||||
private final String blockDataFolder;
|
||||
private final CardDb commonCards;
|
||||
private final CardDb variantCards;
|
||||
@@ -77,6 +79,7 @@ public class StaticData {
|
||||
this.tokenReader = tokenReader;
|
||||
this.editions = new CardEdition.Collection(new CardEdition.Reader(new File(editionFolder)));
|
||||
this.blockDataFolder = blockDataFolder;
|
||||
this.customCardReader = customCardReader;
|
||||
this.allowCustomCardsInDecksConformance = allowCustomCardsInDecksConformance;
|
||||
this.enableSmartCardArtSelection = enableSmartCardArtSelection;
|
||||
this.loadNonLegalCards = loadNonLegalCards;
|
||||
@@ -781,7 +784,6 @@ public class StaticData {
|
||||
Queue<String> TOKEN_Q = new ConcurrentLinkedQueue<>();
|
||||
boolean nifHeader = false;
|
||||
boolean cniHeader = false;
|
||||
final Pattern funnyCardCollectorNumberPattern = Pattern.compile("^F\\d+");
|
||||
for (CardEdition e : editions) {
|
||||
if (CardEdition.Type.FUNNY.equals(e.getType()))
|
||||
continue;
|
||||
@@ -789,13 +791,11 @@ public class StaticData {
|
||||
Map<String, Pair<Boolean, Integer>> cardCount = new HashMap<>();
|
||||
List<CompletableFuture<?>> futures = new ArrayList<>();
|
||||
for (CardEdition.EditionEntry c : e.getObtainableCards()) {
|
||||
int amount = 1;
|
||||
|
||||
if (cardCount.containsKey(c.name())) {
|
||||
amount = cardCount.get(c.name()).getRight() + 1;
|
||||
cardCount.put(c.name(), Pair.of(c.collectorNumber() != null && c.collectorNumber().startsWith("F"), cardCount.get(c.name()).getRight() + 1));
|
||||
} else {
|
||||
cardCount.put(c.name(), Pair.of(c.collectorNumber() != null && c.collectorNumber().startsWith("F"), 1));
|
||||
}
|
||||
|
||||
cardCount.put(c.name(), Pair.of(c.collectorNumber() != null && funnyCardCollectorNumberPattern.matcher(c.collectorNumber()).matches(), amount));
|
||||
}
|
||||
|
||||
// loop through the cards in this edition, considering art variations...
|
||||
@@ -878,7 +878,7 @@ public class StaticData {
|
||||
}
|
||||
}
|
||||
}
|
||||
// stream().toList() causes crash on Android 8-13, use Collectors.toList()
|
||||
// stream().toList() causes crash on Android, use Collectors.toList()
|
||||
List<String> NIF = new ArrayList<>(NIF_Q).stream().sorted().collect(Collectors.toList());
|
||||
List<String> CNI = new ArrayList<>(CNI_Q).stream().sorted().collect(Collectors.toList());
|
||||
List<String> TOK = new ArrayList<>(TOKEN_Q).stream().sorted().collect(Collectors.toList());
|
||||
|
||||
@@ -21,7 +21,6 @@ import com.google.common.collect.ListMultimap;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Multimaps;
|
||||
import forge.ImageKeys;
|
||||
import forge.StaticData;
|
||||
import forge.card.CardEdition.EditionEntry;
|
||||
import forge.card.CardEdition.Type;
|
||||
@@ -45,6 +44,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
public final static char NameSetSeparator = '|';
|
||||
public final static String FlagPrefix = "#";
|
||||
public static final String FlagSeparator = "\t";
|
||||
private final String exlcudedCardName = "Concentrate";
|
||||
private final String exlcudedCardSet = "DS0";
|
||||
|
||||
// need this to obtain cardReference by name+set+artindex
|
||||
private final ListMultimap<String, PaperCard> allCardsByName = Multimaps.newListMultimap(new TreeMap<>(String.CASE_INSENSITIVE_ORDER), Lists::newArrayList);
|
||||
@@ -199,7 +200,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
}
|
||||
|
||||
private static boolean isArtIndex(String s) {
|
||||
return StringUtils.isNumeric(s) && s.length() <= 2; // only artIndex between 1-99
|
||||
return StringUtils.isNumeric(s) && s.length() <= 2 ; // only artIndex between 1-99
|
||||
}
|
||||
|
||||
private static boolean isSetCode(String s) {
|
||||
@@ -240,8 +241,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
setCode = info[index];
|
||||
index++;
|
||||
}
|
||||
if(info.length > index && isArtIndex(info[index].replace(ImageKeys.BACKFACE_POSTFIX, ""))) {
|
||||
artIndex = Integer.parseInt(info[index].replace(ImageKeys.BACKFACE_POSTFIX, ""));
|
||||
if(info.length > index && isArtIndex(info[index])) {
|
||||
artIndex = Integer.parseInt(info[index]);
|
||||
index++;
|
||||
}
|
||||
if(info.length > index && isCollectorNumber(info[index])) {
|
||||
@@ -301,7 +302,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
|
||||
// create faces list from rules
|
||||
for (final CardRules rule : rules.values()) {
|
||||
if (filteredCards.contains(rule.getName()))
|
||||
if (filteredCards.contains(rule.getName()) && !exlcudedCardName.equalsIgnoreCase(rule.getName()))
|
||||
continue;
|
||||
for (ICardFace face : rule.getAllFaces()) {
|
||||
addFaceToDbNames(face);
|
||||
@@ -499,9 +500,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
}
|
||||
|
||||
public void addCard(PaperCard paperCard) {
|
||||
if (filtered.contains(paperCard.getName())) {
|
||||
if (excludeCard(paperCard.getName(), paperCard.getEdition()))
|
||||
return;
|
||||
}
|
||||
|
||||
allCardsByName.put(paperCard.getName(), paperCard);
|
||||
|
||||
@@ -522,6 +522,17 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean excludeCard(String cardName, String cardEdition) {
|
||||
if (filtered.isEmpty())
|
||||
return false;
|
||||
if (filtered.contains(cardName)) {
|
||||
if (exlcudedCardSet.equalsIgnoreCase(cardEdition) && exlcudedCardName.equalsIgnoreCase(cardName))
|
||||
return true;
|
||||
else return !exlcudedCardName.equalsIgnoreCase(cardName);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void reIndex() {
|
||||
uniqueCardsByName.clear();
|
||||
for (Entry<String, Collection<PaperCard>> kv : allCardsByName.asMap().entrySet()) {
|
||||
|
||||
@@ -52,14 +52,6 @@ import java.util.stream.Collectors;
|
||||
*/
|
||||
public final class CardEdition implements Comparable<CardEdition> {
|
||||
|
||||
public DraftOptions getDraftOptions() {
|
||||
return draftOptions;
|
||||
}
|
||||
|
||||
public void setDraftOptions(DraftOptions draftOptions) {
|
||||
this.draftOptions = draftOptions;
|
||||
}
|
||||
|
||||
// immutable
|
||||
public enum Type {
|
||||
UNKNOWN,
|
||||
@@ -283,22 +275,18 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
// Booster/draft info
|
||||
private List<BoosterSlot> boosterSlots = null;
|
||||
private boolean smallSetOverride = false;
|
||||
private String additionalUnlockSet = "";
|
||||
private FoilType foilType = FoilType.NOT_SUPPORTED;
|
||||
|
||||
// Replace all of these things with booster slots
|
||||
private boolean foilAlwaysInCommonSlot = false;
|
||||
private FoilType foilType = FoilType.NOT_SUPPORTED;
|
||||
private double foilChanceInBooster = 0;
|
||||
private double chanceReplaceCommonWith = 0;
|
||||
private String slotReplaceCommonWith = "Common";
|
||||
private String additionalSheetForFoils = "";
|
||||
private String additionalUnlockSet = "";
|
||||
private String boosterMustContain = "";
|
||||
private String boosterReplaceSlotFromPrintSheet = "";
|
||||
private String sheetReplaceCardFromSheet = "";
|
||||
private String sheetReplaceCardFromSheet2 = "";
|
||||
|
||||
// Draft options
|
||||
private DraftOptions draftOptions = null;
|
||||
private String doublePickDuringDraft = "";
|
||||
private String[] chaosDraftThemes = new String[0];
|
||||
|
||||
private final ListMultimap<String, EditionEntry> cardMap;
|
||||
@@ -385,6 +373,7 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
public String getSlotReplaceCommonWith() { return slotReplaceCommonWith; }
|
||||
public String getAdditionalSheetForFoils() { return additionalSheetForFoils; }
|
||||
public String getAdditionalUnlockSet() { return additionalUnlockSet; }
|
||||
public String getDoublePickDuringDraft() { return doublePickDuringDraft; }
|
||||
public String getBoosterMustContain() { return boosterMustContain; }
|
||||
public String getBoosterReplaceSlotFromPrintSheet() { return boosterReplaceSlotFromPrintSheet; }
|
||||
public String getSheetReplaceCardFromSheet() { return sheetReplaceCardFromSheet; }
|
||||
@@ -563,16 +552,26 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
|
||||
public List<PrintSheet> getPrintSheetsBySection() {
|
||||
final CardDb cardDb = StaticData.instance().getCommonCards();
|
||||
Map<String, Integer> cardToIndex = new HashMap<>();
|
||||
|
||||
List<PrintSheet> sheets = Lists.newArrayList();
|
||||
for (Map.Entry<String, java.util.Collection<EditionEntry>> section : cardMap.asMap().entrySet()) {
|
||||
if (section.getKey().equals(EditionSectionWithCollectorNumbers.CONJURED.getName())) {
|
||||
for (String sectionName : cardMap.keySet()) {
|
||||
if (sectionName.equals(EditionSectionWithCollectorNumbers.CONJURED.getName())) {
|
||||
continue;
|
||||
}
|
||||
PrintSheet sheet = new PrintSheet(String.format("%s %s", this.getCode(), section.getKey()));
|
||||
PrintSheet sheet = new PrintSheet(String.format("%s %s", this.getCode(), sectionName));
|
||||
|
||||
for (EditionEntry card : section.getValue()) {
|
||||
sheet.add(cardDb.getCard(card.name, this.getCode(), card.collectorNumber));
|
||||
List<EditionEntry> cards = cardMap.get(sectionName);
|
||||
for (EditionEntry card : cards) {
|
||||
int index = 1;
|
||||
if (cardToIndex.containsKey(card.name)) {
|
||||
index = cardToIndex.get(card.name) + 1;
|
||||
}
|
||||
|
||||
cardToIndex.put(card.name, index);
|
||||
|
||||
PaperCard pCard = cardDb.getCard(card.name, this.getCode(), index);
|
||||
sheet.add(pCard);
|
||||
}
|
||||
|
||||
sheets.add(sheet);
|
||||
@@ -630,7 +629,7 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
* functional variant name - grouping #9
|
||||
*/
|
||||
// "(^(.?[0-9A-Z]+.?))?(([SCURML]) )?(.*)$"
|
||||
"(^(.?[0-9A-Z-]+\\S*[A-Z]*)\\s)?(([SCURML])\\s)?([^@\\$]*)( @([^\\$]*))?( \\$(.+))?$"
|
||||
"(^(.?[0-9A-Z-]+\\S?[A-Z]*)\\s)?(([SCURML])\\s)?([^@\\$]*)( @([^\\$]*))?( \\$(.+))?$"
|
||||
);
|
||||
|
||||
final Pattern tokenPattern = Pattern.compile(
|
||||
@@ -639,7 +638,7 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
* name - grouping #3
|
||||
* artist name - grouping #5
|
||||
*/
|
||||
"(^(.?[0-9A-Z-]+\\S?[A-Z☇]*)\\s)?([^@]*)( @(.*))?$"
|
||||
"(^(.?[0-9A-Z]+\\S?[A-Z]*)\\s)?([^@]*)( @(.*))?$"
|
||||
);
|
||||
|
||||
ListMultimap<String, EditionEntry> cardMap = ArrayListMultimap.create();
|
||||
@@ -660,37 +659,31 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sectionName.endsWith("Types")) {
|
||||
CardType.Helper.parseTypes(sectionName, contents.get(sectionName));
|
||||
} else {
|
||||
// Parse cards
|
||||
// parse sections of the format "<collector number> <rarity> <name>"
|
||||
if (editionSectionsWithCollectorNumbers.contains(sectionName)) {
|
||||
for(String line : contents.get(sectionName)) {
|
||||
Matcher matcher = pattern.matcher(line);
|
||||
|
||||
// parse sections of the format "<collector number> <rarity> <name>"
|
||||
if (editionSectionsWithCollectorNumbers.contains(sectionName)) {
|
||||
for(String line : contents.get(sectionName)) {
|
||||
Matcher matcher = pattern.matcher(line);
|
||||
|
||||
if (!matcher.matches()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String collectorNumber = matcher.group(2);
|
||||
CardRarity r = CardRarity.smartValueOf(matcher.group(4));
|
||||
String cardName = matcher.group(5);
|
||||
String artistName = matcher.group(7);
|
||||
String functionalVariantName = matcher.group(9);
|
||||
EditionEntry cis = new EditionEntry(cardName, collectorNumber, r, artistName, functionalVariantName);
|
||||
|
||||
cardMap.put(sectionName, cis);
|
||||
if (!matcher.matches()) {
|
||||
continue;
|
||||
}
|
||||
} else if (boosterSlotsToParse.contains(sectionName)) {
|
||||
// parse booster slots of the format "Base=N\n|Replace=<amount> <sheet>"
|
||||
boosterSlots.add(BoosterSlot.parseSlot(sectionName, contents.get(sectionName)));
|
||||
} else {
|
||||
// save custom print sheets of the format "<amount> <name>|<setcode>|<art index>"
|
||||
// to parse later when printsheets are loaded lazily (and the cardpool is already initialized)
|
||||
customPrintSheetsToParse.put(sectionName, contents.get(sectionName));
|
||||
|
||||
String collectorNumber = matcher.group(2);
|
||||
CardRarity r = CardRarity.smartValueOf(matcher.group(4));
|
||||
String cardName = matcher.group(5);
|
||||
String artistName = matcher.group(7);
|
||||
String functionalVariantName = matcher.group(9);
|
||||
EditionEntry cis = new EditionEntry(cardName, collectorNumber, r, artistName, functionalVariantName);
|
||||
|
||||
cardMap.put(sectionName, cis);
|
||||
}
|
||||
} else if (boosterSlotsToParse.contains(sectionName)) {
|
||||
// parse booster slots of the format "Base=N\n|Replace=<amount> <sheet>"
|
||||
boosterSlots.add(BoosterSlot.parseSlot(sectionName, contents.get(sectionName)));
|
||||
} else {
|
||||
// save custom print sheets of the format "<amount> <name>|<setcode>|<art index>"
|
||||
// to parse later when printsheets are loaded lazily (and the cardpool is already initialized)
|
||||
customPrintSheetsToParse.put(sectionName, contents.get(sectionName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -819,6 +812,7 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
res.additionalUnlockSet = metadata.get("AdditionalSetUnlockedInQuest", ""); // e.g. Time Spiral Timeshifted (TSB) for Time Spiral
|
||||
|
||||
res.smallSetOverride = metadata.getBoolean("TreatAsSmallSet", false); // for "small" sets with over 200 cards (e.g. Eldritch Moon)
|
||||
res.doublePickDuringDraft = metadata.get("DoublePick", ""); // "FirstPick" or "Always"
|
||||
|
||||
res.boosterMustContain = metadata.get("BoosterMustContain", ""); // e.g. Dominaria guaranteed legendary creature
|
||||
res.boosterReplaceSlotFromPrintSheet = metadata.get("BoosterReplaceSlotFromPrintSheet", ""); // e.g. Zendikar Rising guaranteed double-faced card
|
||||
@@ -826,23 +820,6 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
res.sheetReplaceCardFromSheet2 = metadata.get("SheetReplaceCardFromSheet2", "");
|
||||
res.chaosDraftThemes = metadata.get("ChaosDraftThemes", "").split(";"); // semicolon separated list of theme names
|
||||
|
||||
// Draft options
|
||||
String doublePick = metadata.get("DoublePick", "Never");
|
||||
int maxPodSize = metadata.getInt("MaxPodSize", 8);
|
||||
int recommendedPodSize = metadata.getInt("RecommendedPodSize", 8);
|
||||
int maxMatchPlayers = metadata.getInt("MaxMatchPlayers", 2);
|
||||
String deckType = metadata.get("DeckType", "Normal");
|
||||
String freeCommander = metadata.get("FreeCommander", "");
|
||||
|
||||
res.draftOptions = new DraftOptions(
|
||||
doublePick,
|
||||
maxPodSize,
|
||||
recommendedPodSize,
|
||||
maxMatchPlayers,
|
||||
deckType,
|
||||
freeCommander
|
||||
);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -873,7 +850,7 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
@Override
|
||||
public void add(CardEdition item) { //Even though we want it to be read only, make an exception for custom content.
|
||||
if(lock) throw new UnsupportedOperationException("This is a read-only storage");
|
||||
else map.put(item.getCode(), item);
|
||||
else map.put(item.getName(), item);
|
||||
}
|
||||
public void append(CardEdition.Collection C){ //Append custom editions
|
||||
if (lock) throw new UnsupportedOperationException("This is a read-only storage");
|
||||
@@ -1018,13 +995,16 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
|
||||
public static final Predicate<CardEdition> HAS_BOOSTER_BOX = edition -> edition.getBoosterBoxCount() > 0;
|
||||
|
||||
@Deprecated //Use CardEdition::hasBasicLands and a nonnull test.
|
||||
public static final Predicate<CardEdition> hasBasicLands = ed -> {
|
||||
if (ed == null) {
|
||||
// Happens for new sets with "???" code
|
||||
return false;
|
||||
}
|
||||
return ed.hasBasicLands();
|
||||
for(String landName : MagicColor.Constant.BASIC_LANDS) {
|
||||
if (null == StaticData.instance().getCommonCards().getCard(landName, ed.getCode(), 0))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1045,7 +1025,7 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
|
||||
public boolean hasBasicLands() {
|
||||
for(String landName : MagicColor.Constant.BASIC_LANDS) {
|
||||
if (this.getCardInSet(landName).isEmpty())
|
||||
if (null == StaticData.instance().getCommonCards().getCard(landName, this.getCode(), 0))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -53,7 +53,6 @@ public final class CardRules implements ICardCharacteristics {
|
||||
private boolean addsWildCardColor;
|
||||
private int setColorID;
|
||||
private boolean custom;
|
||||
private boolean unsupported;
|
||||
private String path;
|
||||
|
||||
public CardRules(ICardFace[] faces, CardSplitType altMode, CardAiHints cah) {
|
||||
@@ -167,10 +166,6 @@ public final class CardRules implements ICardCharacteristics {
|
||||
return Iterables.concat(Arrays.asList(mainPart, otherPart), specializedParts.values());
|
||||
}
|
||||
|
||||
public boolean isTransformable() {
|
||||
return CardSplitType.Transform == getSplitType() || CardSplitType.Modal == getSplitType();
|
||||
}
|
||||
|
||||
public ICardFace getWSpecialize() {
|
||||
return specializedParts.get(CardStateName.SpecializeW);
|
||||
}
|
||||
@@ -207,9 +202,7 @@ public final class CardRules implements ICardCharacteristics {
|
||||
}
|
||||
|
||||
public boolean isCustom() { return custom; }
|
||||
public void setCustom() { custom = true; }
|
||||
|
||||
public boolean isUnsupported() { return unsupported; }
|
||||
public void setCustom() { custom = true; }
|
||||
|
||||
@Override
|
||||
public CardType getType() {
|
||||
@@ -324,12 +317,6 @@ public final class CardRules implements ICardCharacteristics {
|
||||
}
|
||||
if (hasKeyword("Friends forever") && b.hasKeyword("Friends forever")) {
|
||||
legal = true; // Stranger Things Secret Lair gimmick partner commander
|
||||
}
|
||||
if (hasKeyword("Partner - Survivors") && b.hasKeyword("Partner - Survivors")) {
|
||||
legal = true; // The Last of Us Secret Lair gimmick partner commander
|
||||
}
|
||||
if (hasKeyword("Partner - Father & Son") && b.hasKeyword("Partner - Father & Son")) {
|
||||
legal = true; // God of War Secret Lair gimmick partner commander
|
||||
}
|
||||
if (hasKeyword("Choose a Background") && b.canBeBackground()
|
||||
|| b.hasKeyword("Choose a Background") && canBeBackground()) {
|
||||
@@ -348,7 +335,6 @@ public final class CardRules implements ICardCharacteristics {
|
||||
}
|
||||
return canBeCommander() && (hasKeyword("Partner") || !this.partnerWith.isEmpty() ||
|
||||
hasKeyword("Friends forever") || hasKeyword("Choose a Background") ||
|
||||
hasKeyword("Partner - Father & Son") || hasKeyword("Partner - Survivors") ||
|
||||
hasKeyword("Doctor's companion") || isDoctor());
|
||||
}
|
||||
|
||||
@@ -357,21 +343,16 @@ public final class CardRules implements ICardCharacteristics {
|
||||
}
|
||||
|
||||
public boolean isDoctor() {
|
||||
Set<String> subtypes = new HashSet<>();
|
||||
for (String type : mainPart.getType().getSubtypes()) {
|
||||
subtypes.add(type);
|
||||
if (!type.equals("Time Lord") && !type.equals("Doctor")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return subtypes.size() == 2 &&
|
||||
subtypes.contains("Time Lord") &&
|
||||
subtypes.contains("Doctor");
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean canBeOathbreaker() {
|
||||
CardType type = mainPart.getType();
|
||||
if (mainPart.getOracleText().contains("can be your commander")) {
|
||||
return true;
|
||||
}
|
||||
return type.isPlaneswalker();
|
||||
}
|
||||
|
||||
@@ -824,8 +805,6 @@ public final class CardRules implements ICardCharacteristics {
|
||||
faces[0].assignMissingFields();
|
||||
final CardRules result = new CardRules(faces, CardSplitType.None, cah);
|
||||
|
||||
result.unsupported = true;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -189,38 +189,6 @@ public final class CardRulesPredicates {
|
||||
return card -> card.getType().hasSupertype(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a Predicate that matches cards that are of the split type.
|
||||
*/
|
||||
public static Predicate<CardRules> isSplitType(final CardSplitType type) {
|
||||
return card -> card.getSplitType().equals(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a Predicate that matches cards that are vanilla.
|
||||
*/
|
||||
public static Predicate<CardRules> isVanilla() {
|
||||
return card -> {
|
||||
if (!(card.getType().isCreature() || card.getType().isLand()) ||
|
||||
card.getSplitType() != CardSplitType.None ||
|
||||
card.hasFunctionalVariants()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ICardFace mainPart = card.getMainPart();
|
||||
|
||||
boolean hasAny =
|
||||
mainPart.getKeywords().iterator().hasNext() ||
|
||||
mainPart.getAbilities().iterator().hasNext() ||
|
||||
mainPart.getStaticAbilities().iterator().hasNext() ||
|
||||
mainPart.getTriggers().iterator().hasNext() ||
|
||||
(mainPart.getDraftActions() != null && mainPart.getDraftActions().iterator().hasNext()) ||
|
||||
mainPart.getReplacements().iterator().hasNext();
|
||||
|
||||
return !hasAny;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for color.
|
||||
*
|
||||
|
||||
@@ -7,13 +7,13 @@ import java.util.EnumSet;
|
||||
public enum CardSplitType
|
||||
{
|
||||
None(FaceSelectionMethod.USE_PRIMARY_FACE, null),
|
||||
Transform(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Backside),
|
||||
Transform(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Transformed),
|
||||
Meld(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Meld),
|
||||
Split(FaceSelectionMethod.COMBINE, CardStateName.RightSplit),
|
||||
Flip(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Flipped),
|
||||
Adventure(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Secondary),
|
||||
Omen(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Secondary),
|
||||
Modal(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Backside),
|
||||
Modal(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Modal),
|
||||
Specialize(FaceSelectionMethod.USE_ACTIVE_FACE, null);
|
||||
|
||||
public static final EnumSet<CardSplitType> DUAL_FACED_CARDS = EnumSet.of(
|
||||
|
||||
@@ -5,11 +5,12 @@ public enum CardStateName {
|
||||
Original,
|
||||
FaceDown,
|
||||
Flipped,
|
||||
Backside,
|
||||
Transformed,
|
||||
Meld,
|
||||
LeftSplit,
|
||||
RightSplit,
|
||||
Secondary,
|
||||
Modal,
|
||||
EmptyRoom,
|
||||
SpecializeW,
|
||||
SpecializeU,
|
||||
@@ -41,7 +42,7 @@ public enum CardStateName {
|
||||
return CardStateName.Flipped;
|
||||
}
|
||||
if ("DoubleFaced".equalsIgnoreCase(value)) {
|
||||
return CardStateName.Backside;
|
||||
return CardStateName.Transformed;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("No element named " + value + " in enum CardCharactersticName");
|
||||
|
||||
@@ -1066,74 +1066,4 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
|
||||
return type;
|
||||
}
|
||||
|
||||
public static class Helper {
|
||||
public static final void parseTypes(String sectionName, List<String> content) {
|
||||
Set<String> addToSection = null;
|
||||
|
||||
switch (sectionName) {
|
||||
case "BasicTypes":
|
||||
addToSection = CardType.Constant.BASIC_TYPES;
|
||||
break;
|
||||
case "LandTypes":
|
||||
addToSection = CardType.Constant.LAND_TYPES;
|
||||
break;
|
||||
case "CreatureTypes":
|
||||
addToSection = CardType.Constant.CREATURE_TYPES;
|
||||
break;
|
||||
case "SpellTypes":
|
||||
addToSection = CardType.Constant.SPELL_TYPES;
|
||||
break;
|
||||
case "EnchantmentTypes":
|
||||
addToSection = CardType.Constant.ENCHANTMENT_TYPES;
|
||||
break;
|
||||
case "ArtifactTypes":
|
||||
addToSection = CardType.Constant.ARTIFACT_TYPES;
|
||||
break;
|
||||
case "WalkerTypes":
|
||||
addToSection = CardType.Constant.WALKER_TYPES;
|
||||
break;
|
||||
case "DungeonTypes":
|
||||
addToSection = CardType.Constant.DUNGEON_TYPES;
|
||||
break;
|
||||
case "BattleTypes":
|
||||
addToSection = CardType.Constant.BATTLE_TYPES;
|
||||
break;
|
||||
case "PlanarTypes":
|
||||
addToSection = CardType.Constant.PLANAR_TYPES;
|
||||
break;
|
||||
}
|
||||
|
||||
if (addToSection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(String line : content) {
|
||||
if (line.length() == 0) continue;
|
||||
|
||||
if (line.contains(":")) {
|
||||
String[] k = line.split(":");
|
||||
|
||||
if (addToSection.contains(k[0])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
addToSection.add(k[0]);
|
||||
CardType.Constant.pluralTypes.put(k[0], k[1]);
|
||||
|
||||
if (k[0].contains(" ")) {
|
||||
CardType.Constant.MultiwordTypes.add(k[0]);
|
||||
}
|
||||
} else {
|
||||
if (addToSection.contains(line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
addToSection.add(line);
|
||||
if (line.contains(" ")) {
|
||||
CardType.Constant.MultiwordTypes.add(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ package forge.card;
|
||||
import com.google.common.collect.UnmodifiableIterator;
|
||||
import forge.card.MagicColor.Color;
|
||||
import forge.card.mana.ManaCost;
|
||||
import forge.card.mana.ManaCostShard;
|
||||
import forge.util.BinaryUtil;
|
||||
|
||||
import java.io.Serializable;
|
||||
@@ -40,95 +41,25 @@ import java.util.stream.Stream;
|
||||
public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Serializable {
|
||||
private static final long serialVersionUID = 794691267379929080L;
|
||||
|
||||
// needs to be before other static
|
||||
private static final ColorSet[] cache = new ColorSet[MagicColor.ALL_COLORS + 1];
|
||||
static {
|
||||
byte COLORLESS = MagicColor.COLORLESS;
|
||||
byte WHITE = MagicColor.WHITE;
|
||||
byte BLUE = MagicColor.BLUE;
|
||||
byte BLACK = MagicColor.BLACK;
|
||||
byte RED = MagicColor.RED;
|
||||
byte GREEN = MagicColor.GREEN;
|
||||
Color C = Color.COLORLESS;
|
||||
Color W = Color.WHITE;
|
||||
Color U = Color.BLUE;
|
||||
Color B = Color.BLACK;
|
||||
Color R = Color.RED;
|
||||
Color G = Color.GREEN;
|
||||
|
||||
//colorless
|
||||
cache[COLORLESS] = new ColorSet(C);
|
||||
|
||||
//mono-color
|
||||
cache[WHITE] = new ColorSet(W);
|
||||
cache[BLUE] = new ColorSet(U);
|
||||
cache[BLACK] = new ColorSet(B);
|
||||
cache[RED] = new ColorSet(R);
|
||||
cache[GREEN] = new ColorSet(G);
|
||||
|
||||
//two-color
|
||||
cache[WHITE | BLUE] = new ColorSet(W, U);
|
||||
cache[WHITE | BLACK] = new ColorSet(W, B);
|
||||
cache[BLUE | BLACK] = new ColorSet(U, B);
|
||||
cache[BLUE | RED] = new ColorSet(U, R);
|
||||
cache[BLACK | RED] = new ColorSet(B, R);
|
||||
cache[BLACK | GREEN] = new ColorSet(B, G);
|
||||
cache[RED | GREEN] = new ColorSet(R, G);
|
||||
cache[RED | WHITE] = new ColorSet(R, W);
|
||||
cache[GREEN | WHITE] = new ColorSet(G, W);
|
||||
cache[GREEN | BLUE] = new ColorSet(G, U);
|
||||
|
||||
//three-color
|
||||
cache[WHITE | BLUE | BLACK] = new ColorSet(W, U, B);
|
||||
cache[WHITE | BLACK | GREEN] = new ColorSet(W, B, G);
|
||||
cache[BLUE | BLACK | RED] = new ColorSet(U, B, R);
|
||||
cache[BLUE | RED | WHITE] = new ColorSet(U, R, W);
|
||||
cache[BLACK | RED | GREEN] = new ColorSet(B, R, G);
|
||||
cache[BLACK | GREEN | BLUE] = new ColorSet(B, G, U);
|
||||
cache[RED | GREEN | WHITE] = new ColorSet(R, G, W);
|
||||
cache[RED | WHITE | BLACK] = new ColorSet(R, W, B);
|
||||
cache[GREEN | WHITE | BLUE] = new ColorSet(G, W, U);
|
||||
cache[GREEN | BLUE | RED] = new ColorSet(G, U, R);
|
||||
|
||||
//four-color
|
||||
cache[WHITE | BLUE | BLACK | RED] = new ColorSet(W, U, B, R);
|
||||
cache[BLUE | BLACK | RED | GREEN] = new ColorSet(U, B, R, G);
|
||||
cache[BLACK | RED | GREEN | WHITE] = new ColorSet(B, R, G, W);
|
||||
cache[RED | GREEN | WHITE | BLUE] = new ColorSet(R, G, W, U);
|
||||
cache[GREEN | WHITE | BLUE | BLACK] = new ColorSet(G, W, U, B);
|
||||
|
||||
//five-color
|
||||
cache[WHITE | BLUE | BLACK | RED | GREEN] = new ColorSet(W, U, B, R, G);
|
||||
}
|
||||
|
||||
private final Collection<Color> orderedShards;
|
||||
private final byte myColor;
|
||||
private final float orderWeight;
|
||||
private final Set<Color> enumSet;
|
||||
private final String desc;
|
||||
|
||||
private static final ColorSet[] cache = new ColorSet[32];
|
||||
|
||||
public static final ColorSet ALL_COLORS = fromMask(MagicColor.ALL_COLORS);
|
||||
public static final ColorSet NO_COLORS = fromMask(MagicColor.COLORLESS);
|
||||
private static final ColorSet NO_COLORS = fromMask(MagicColor.COLORLESS);
|
||||
|
||||
private ColorSet(final Color... ordered) {
|
||||
this.orderedShards = Arrays.asList(ordered);
|
||||
this.myColor = orderedShards.stream().map(Color::getColorMask).reduce((byte)0, (a, b) -> (byte)(a | b));
|
||||
private ColorSet(final byte mask) {
|
||||
this.myColor = mask;
|
||||
this.orderWeight = this.getOrderWeight();
|
||||
this.enumSet = EnumSet.copyOf(orderedShards);
|
||||
this.desc = orderedShards.stream().map(Color::getShortName).collect(Collectors.joining());
|
||||
}
|
||||
|
||||
public static ColorSet fromMask(final int mask) {
|
||||
final int mask32 = mask & MagicColor.ALL_COLORS;
|
||||
return cache[mask32];
|
||||
}
|
||||
|
||||
public static ColorSet fromEnums(final Color... colors) {
|
||||
byte mask = 0;
|
||||
for (Color e : colors) {
|
||||
mask |= e.getColorMask();
|
||||
if (cache[mask32] == null) {
|
||||
cache[mask32] = new ColorSet((byte) mask32);
|
||||
}
|
||||
return fromMask(mask);
|
||||
return cache[mask32];
|
||||
}
|
||||
|
||||
public static ColorSet fromNames(final String... colors) {
|
||||
@@ -362,7 +293,17 @@ public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Ser
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return desc;
|
||||
final ManaCostShard[] orderedShards = getOrderedShards();
|
||||
return Arrays.stream(orderedShards).map(ManaCostShard::toShortString).collect(Collectors.joining());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the null color.
|
||||
*
|
||||
* @return the nullColor
|
||||
*/
|
||||
public static ColorSet getNullColor() {
|
||||
return NO_COLORS;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -384,7 +325,16 @@ public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Ser
|
||||
}
|
||||
|
||||
public Set<Color> toEnumSet() {
|
||||
return EnumSet.copyOf(enumSet);
|
||||
if (isColorless()) {
|
||||
return EnumSet.of(Color.COLORLESS);
|
||||
}
|
||||
List<Color> list = new ArrayList<>();
|
||||
for (Color c : Color.values()) {
|
||||
if (hasAnyColor(c.getColormask())) {
|
||||
list.add(c);
|
||||
}
|
||||
}
|
||||
return EnumSet.copyOf(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -422,12 +372,72 @@ public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Ser
|
||||
}
|
||||
}
|
||||
|
||||
public Stream<Color> stream() {
|
||||
public Stream<MagicColor.Color> stream() {
|
||||
return this.toEnumSet().stream();
|
||||
}
|
||||
|
||||
//Get array of mana cost shards for color set in the proper order
|
||||
public Collection<Color> getOrderedColors() {
|
||||
return orderedShards;
|
||||
public ManaCostShard[] getOrderedShards() {
|
||||
return shardOrderLookup[myColor];
|
||||
}
|
||||
|
||||
private static final ManaCostShard[][] shardOrderLookup = new ManaCostShard[MagicColor.ALL_COLORS + 1][];
|
||||
static {
|
||||
byte COLORLESS = MagicColor.COLORLESS;
|
||||
byte WHITE = MagicColor.WHITE;
|
||||
byte BLUE = MagicColor.BLUE;
|
||||
byte BLACK = MagicColor.BLACK;
|
||||
byte RED = MagicColor.RED;
|
||||
byte GREEN = MagicColor.GREEN;
|
||||
ManaCostShard C = ManaCostShard.COLORLESS;
|
||||
ManaCostShard W = ManaCostShard.WHITE;
|
||||
ManaCostShard U = ManaCostShard.BLUE;
|
||||
ManaCostShard B = ManaCostShard.BLACK;
|
||||
ManaCostShard R = ManaCostShard.RED;
|
||||
ManaCostShard G = ManaCostShard.GREEN;
|
||||
|
||||
//colorless
|
||||
shardOrderLookup[COLORLESS] = new ManaCostShard[] { C };
|
||||
|
||||
//mono-color
|
||||
shardOrderLookup[WHITE] = new ManaCostShard[] { W };
|
||||
shardOrderLookup[BLUE] = new ManaCostShard[] { U };
|
||||
shardOrderLookup[BLACK] = new ManaCostShard[] { B };
|
||||
shardOrderLookup[RED] = new ManaCostShard[] { R };
|
||||
shardOrderLookup[GREEN] = new ManaCostShard[] { G };
|
||||
|
||||
//two-color
|
||||
shardOrderLookup[WHITE | BLUE] = new ManaCostShard[] { W, U };
|
||||
shardOrderLookup[WHITE | BLACK] = new ManaCostShard[] { W, B };
|
||||
shardOrderLookup[BLUE | BLACK] = new ManaCostShard[] { U, B };
|
||||
shardOrderLookup[BLUE | RED] = new ManaCostShard[] { U, R };
|
||||
shardOrderLookup[BLACK | RED] = new ManaCostShard[] { B, R };
|
||||
shardOrderLookup[BLACK | GREEN] = new ManaCostShard[] { B, G };
|
||||
shardOrderLookup[RED | GREEN] = new ManaCostShard[] { R, G };
|
||||
shardOrderLookup[RED | WHITE] = new ManaCostShard[] { R, W };
|
||||
shardOrderLookup[GREEN | WHITE] = new ManaCostShard[] { G, W };
|
||||
shardOrderLookup[GREEN | BLUE] = new ManaCostShard[] { G, U };
|
||||
|
||||
//three-color
|
||||
shardOrderLookup[WHITE | BLUE | BLACK] = new ManaCostShard[] { W, U, B };
|
||||
shardOrderLookup[WHITE | BLACK | GREEN] = new ManaCostShard[] { W, B, G };
|
||||
shardOrderLookup[BLUE | BLACK | RED] = new ManaCostShard[] { U, B, R };
|
||||
shardOrderLookup[BLUE | RED | WHITE] = new ManaCostShard[] { U, R, W };
|
||||
shardOrderLookup[BLACK | RED | GREEN] = new ManaCostShard[] { B, R, G };
|
||||
shardOrderLookup[BLACK | GREEN | BLUE] = new ManaCostShard[] { B, G, U };
|
||||
shardOrderLookup[RED | GREEN | WHITE] = new ManaCostShard[] { R, G, W };
|
||||
shardOrderLookup[RED | WHITE | BLACK] = new ManaCostShard[] { R, W, B };
|
||||
shardOrderLookup[GREEN | WHITE | BLUE] = new ManaCostShard[] { G, W, U };
|
||||
shardOrderLookup[GREEN | BLUE | RED] = new ManaCostShard[] { G, U, R };
|
||||
|
||||
//four-color
|
||||
shardOrderLookup[WHITE | BLUE | BLACK | RED] = new ManaCostShard[] { W, U, B, R };
|
||||
shardOrderLookup[BLUE | BLACK | RED | GREEN] = new ManaCostShard[] { U, B, R, G };
|
||||
shardOrderLookup[BLACK | RED | GREEN | WHITE] = new ManaCostShard[] { B, R, G, W };
|
||||
shardOrderLookup[RED | GREEN | WHITE | BLUE] = new ManaCostShard[] { R, G, W, U };
|
||||
shardOrderLookup[GREEN | WHITE | BLUE | BLACK] = new ManaCostShard[] { G, W, U, B };
|
||||
|
||||
//five-color
|
||||
shardOrderLookup[WHITE | BLUE | BLACK | RED | GREEN] = new ManaCostShard[] { W, U, B, R, G };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
package forge.card;
|
||||
|
||||
public class DraftOptions {
|
||||
public enum DoublePick {
|
||||
NEVER,
|
||||
FIRST_PICK, // only first pick each pack
|
||||
WHEN_POD_SIZE_IS_4, // only when pod size is 4, so you can pick two cards each time
|
||||
ALWAYS // each time you receive a pack, you can pick two cards
|
||||
};
|
||||
public enum DeckType {
|
||||
Normal, // Standard deck, usually 40 cards
|
||||
Commander // Special deck type for Commander format. Important for selection/construction
|
||||
}
|
||||
|
||||
private DoublePick doublePick = DoublePick.NEVER;
|
||||
private final int maxPodSize; // Usually 8, but could be smaller for cubes. I guess it could be larger too
|
||||
private final int recommendedPodSize; // Usually 8, but is 4 for new double pick
|
||||
private final int maxMatchPlayers; // Usually 2, but 4 for things like Commander or Conspiracy
|
||||
private final DeckType deckType; // Normal or Commander
|
||||
private final String freeCommander;
|
||||
|
||||
public DraftOptions(String doublePickOption, int maxPodSize, int recommendedPodSize, int maxMatchPlayers, String deckType, String freeCommander) {
|
||||
this.maxPodSize = maxPodSize;
|
||||
this.recommendedPodSize = recommendedPodSize;
|
||||
this.maxMatchPlayers = maxMatchPlayers;
|
||||
this.deckType = DeckType.valueOf(deckType);
|
||||
this.freeCommander = freeCommander;
|
||||
if (doublePickOption != null) {
|
||||
switch (doublePickOption.toLowerCase()) {
|
||||
case "firstpick":
|
||||
doublePick = DoublePick.FIRST_PICK;
|
||||
break;
|
||||
case "always":
|
||||
doublePick = DoublePick.ALWAYS;
|
||||
break;
|
||||
case "whenpodsizeis4":
|
||||
doublePick = DoublePick.WHEN_POD_SIZE_IS_4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
public int getMaxPodSize() {
|
||||
return maxPodSize;
|
||||
}
|
||||
public int getRecommendedPodSize() {
|
||||
return recommendedPodSize;
|
||||
}
|
||||
public DoublePick getDoublePick() {
|
||||
return doublePick;
|
||||
}
|
||||
|
||||
public DoublePick isDoublePick(int podSize) {
|
||||
if (doublePick == DoublePick.WHEN_POD_SIZE_IS_4) {
|
||||
if (podSize != 4) {
|
||||
return DoublePick.NEVER;
|
||||
}
|
||||
// only when pod size is 4, so you can pick two cards each time
|
||||
return DoublePick.ALWAYS;
|
||||
}
|
||||
|
||||
return doublePick;
|
||||
}
|
||||
|
||||
|
||||
public int getMaxMatchPlayers() {
|
||||
return maxMatchPlayers;
|
||||
}
|
||||
public DeckType getDeckType() {
|
||||
return deckType;
|
||||
}
|
||||
public String getFreeCommander() {
|
||||
return freeCommander;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
package forge.card;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import forge.util.ITranslatable;
|
||||
import forge.util.Localizer;
|
||||
import forge.deck.DeckRecognizer;
|
||||
|
||||
/**
|
||||
* Holds byte values for each color magic has.
|
||||
@@ -159,24 +157,21 @@ public final class MagicColor {
|
||||
}
|
||||
}
|
||||
|
||||
public enum Color implements ITranslatable {
|
||||
WHITE(Constant.WHITE, MagicColor.WHITE, "W", "lblWhite"),
|
||||
BLUE(Constant.BLUE, MagicColor.BLUE, "U", "lblBlue"),
|
||||
BLACK(Constant.BLACK, MagicColor.BLACK, "B", "lblBlack"),
|
||||
RED(Constant.RED, MagicColor.RED, "R", "lblRed"),
|
||||
GREEN(Constant.GREEN, MagicColor.GREEN, "G", "lblGreen"),
|
||||
COLORLESS(Constant.COLORLESS, MagicColor.COLORLESS, "C", "lblColorless");
|
||||
public enum Color {
|
||||
WHITE(Constant.WHITE, MagicColor.WHITE, "{W}"),
|
||||
BLUE(Constant.BLUE, MagicColor.BLUE, "{U}"),
|
||||
BLACK(Constant.BLACK, MagicColor.BLACK, "{B}"),
|
||||
RED(Constant.RED, MagicColor.RED, "{R}"),
|
||||
GREEN(Constant.GREEN, MagicColor.GREEN, "{G}"),
|
||||
COLORLESS(Constant.COLORLESS, MagicColor.COLORLESS, "{C}");
|
||||
|
||||
private final String name, shortName, symbol;
|
||||
private final String label;
|
||||
private final String name, symbol;
|
||||
private final byte colormask;
|
||||
|
||||
Color(String name0, byte colormask0, String shortName, String label) {
|
||||
Color(String name0, byte colormask0, String symbol0) {
|
||||
name = name0;
|
||||
colormask = colormask0;
|
||||
this.shortName = shortName;
|
||||
symbol = "{" + shortName + "}";
|
||||
this.label = label;
|
||||
symbol = symbol0;
|
||||
}
|
||||
|
||||
public static Color fromByte(final byte color) {
|
||||
@@ -190,25 +185,25 @@ public final class MagicColor {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
public String getShortName() {
|
||||
return shortName;
|
||||
|
||||
public String getLocalizedName() {
|
||||
//Should probably move some of this logic back here, or at least to a more general location.
|
||||
return DeckRecognizer.getLocalisedMagicColorName(getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTranslatedName() {
|
||||
return Localizer.getInstance().getMessage(label);
|
||||
}
|
||||
|
||||
public byte getColorMask() {
|
||||
public byte getColormask() {
|
||||
return colormask;
|
||||
}
|
||||
public String getSymbol() {
|
||||
return symbol;
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* TODO: Write javadoc for this type.
|
||||
@@ -69,13 +68,6 @@ public class PrintSheet {
|
||||
cardsWithWeights.remove(card);
|
||||
}
|
||||
|
||||
public boolean contains(PaperCard pc) {
|
||||
return cardsWithWeights.contains(pc);
|
||||
}
|
||||
public PaperCard find(Predicate<PaperCard> filter) {
|
||||
return cardsWithWeights.find(filter);
|
||||
}
|
||||
|
||||
private PaperCard fetchRoulette(int start, int roulette, Collection<PaperCard> toSkip) {
|
||||
int sum = start;
|
||||
boolean isSecondRun = start > 0;
|
||||
@@ -93,6 +85,15 @@ public class PrintSheet {
|
||||
return fetchRoulette(sum + 1, roulette, toSkip); // start over from beginning, in case last cards were to skip
|
||||
}
|
||||
|
||||
public List<PaperCard> all() {
|
||||
List<PaperCard> result = new ArrayList<>();
|
||||
for (Entry<PaperCard, Integer> kv : cardsWithWeights) {
|
||||
for (int i = 0; i < kv.getValue(); i++) {
|
||||
result.add(kv.getKey());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
public boolean containsCardNamed(String name,int atLeast) {
|
||||
int count=0;
|
||||
for (Entry<PaperCard, Integer> kv : cardsWithWeights) {
|
||||
@@ -143,7 +144,7 @@ public class PrintSheet {
|
||||
return cardsWithWeights.isEmpty();
|
||||
}
|
||||
|
||||
public List<PaperCard> toFlatList() {
|
||||
public Iterable<PaperCard> toFlatList() {
|
||||
return cardsWithWeights.toFlatList();
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import java.util.function.Predicate;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
public class CardPool extends ItemPool<PaperCard> {
|
||||
private static final long serialVersionUID = -5379091255613968393L;
|
||||
|
||||
@@ -77,20 +78,12 @@ public class CardPool extends ItemPool<PaperCard> {
|
||||
Map<String, CardDb> dbs = StaticData.instance().getAvailableDatabases();
|
||||
for (Map.Entry<String, CardDb> entry: dbs.entrySet()){
|
||||
CardDb db = entry.getValue();
|
||||
|
||||
PaperCard paperCard = db.getCard(cardName, setCode, collectorNumber, flags);
|
||||
if (paperCard != null) {
|
||||
this.add(paperCard, amount);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get non-Alchemy version if it cannot find it.
|
||||
if (cardName.startsWith("A-")) {
|
||||
System.out.println("Alchemy card not found for '" + cardName + "'. Trying to get its non-Alchemy equivalent.");
|
||||
cardName = cardName.replaceFirst("A-", "");
|
||||
}
|
||||
|
||||
//Failed to find it. Fall back accordingly?
|
||||
this.add(cardName, setCode, IPaperCard.NO_ART_INDEX, amount, addAny, flags);
|
||||
}
|
||||
@@ -426,12 +419,6 @@ public class CardPool extends ItemPool<PaperCard> {
|
||||
return pool;
|
||||
}
|
||||
|
||||
public static CardPool fromSingleCardRequest(String cardRequest) {
|
||||
if(StringUtils.isBlank(cardRequest))
|
||||
return new CardPool();
|
||||
return fromCardList(List.of(cardRequest));
|
||||
}
|
||||
|
||||
public static List<Pair<String, Integer>> processCardList(final Iterable<String> lines) {
|
||||
List<Pair<String, Integer>> cardRequests = new ArrayList<>();
|
||||
if (lines == null)
|
||||
@@ -481,7 +468,6 @@ public class CardPool extends ItemPool<PaperCard> {
|
||||
* @param predicate the Predicate to apply to this CardPool
|
||||
* @return a new CardPool made from this CardPool with only the cards that agree with the provided Predicate
|
||||
*/
|
||||
@Override
|
||||
public CardPool getFilteredPool(Predicate<PaperCard> predicate) {
|
||||
CardPool filteredPool = new CardPool();
|
||||
for (PaperCard c : this.items.keySet()) {
|
||||
|
||||
@@ -28,8 +28,6 @@ import forge.item.PaperCard;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import java.io.ObjectStreamException;
|
||||
import java.io.Serial;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
@@ -115,20 +113,6 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
|
||||
return parts.get(DeckSection.Main);
|
||||
}
|
||||
|
||||
public Pair<Deck, List<PaperCard>> getValid() {
|
||||
List<PaperCard> unsupported = new ArrayList<>();
|
||||
for (Entry<DeckSection, CardPool> kv : parts.entrySet()) {
|
||||
CardPool pool = kv.getValue();
|
||||
for (Entry<PaperCard, Integer> pc : pool) {
|
||||
if (pc.getKey().getRules() != null && pc.getKey().getRules().isUnsupported()) {
|
||||
unsupported.add(pc.getKey());
|
||||
pool.remove(pc.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
return Pair.of(this, unsupported);
|
||||
}
|
||||
|
||||
public List<PaperCard> getCommanders() {
|
||||
List<PaperCard> result = Lists.newArrayList();
|
||||
final CardPool cp = get(DeckSection.Commander);
|
||||
@@ -224,19 +208,14 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
|
||||
super.cloneFieldsTo(clone);
|
||||
final Deck result = (Deck) clone;
|
||||
loadDeferredSections();
|
||||
// parts shouldn't be null
|
||||
if (parts != null) {
|
||||
for (Entry<DeckSection, CardPool> kv : parts.entrySet()) {
|
||||
CardPool cp = new CardPool();
|
||||
result.parts.put(kv.getKey(), cp);
|
||||
cp.addAll(kv.getValue());
|
||||
}
|
||||
for (Entry<DeckSection, CardPool> kv : parts.entrySet()) {
|
||||
CardPool cp = new CardPool();
|
||||
result.parts.put(kv.getKey(), cp);
|
||||
cp.addAll(kv.getValue());
|
||||
}
|
||||
result.setAiHints(StringUtils.join(aiHints, " | "));
|
||||
result.setDraftNotes(draftNotes);
|
||||
//noinspection ConstantValue
|
||||
if(tags != null) //Can happen deserializing old Decks.
|
||||
result.tags.addAll(this.tags);
|
||||
tags.addAll(result.getTags());
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -542,17 +521,6 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
|
||||
return sum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of copies of this exact card print across all deck sections.
|
||||
*/
|
||||
public int count(PaperCard card) {
|
||||
int sum = 0;
|
||||
for (Entry<DeckSection, CardPool> section : this) {
|
||||
sum += section.getValue().count(card);
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
public void setAiHints(String aiHintsInfo) {
|
||||
if (aiHintsInfo == null || aiHintsInfo.trim().isEmpty()) {
|
||||
return;
|
||||
@@ -646,14 +614,6 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
|
||||
return this;
|
||||
}
|
||||
|
||||
@Serial
|
||||
private Object readResolve() throws ObjectStreamException {
|
||||
//If we deserialized an old deck that doesn't have tags, fix it here.
|
||||
if(this.tags == null)
|
||||
return new Deck(this, this.getName() == null ? "" : this.getName());
|
||||
return this;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
@@ -703,4 +663,4 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
|
||||
}
|
||||
return totalCount == 0 ? 0 : Math.round(totalCMC / totalCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,8 +32,11 @@ import forge.util.TextUtil;
|
||||
import org.apache.commons.lang3.Range;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
@@ -57,13 +60,6 @@ public enum DeckFormat {
|
||||
//Limited contraption decks have no restrictions.
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getExtraSectionMaxCopies(DeckSection section) {
|
||||
if(section == DeckSection.Attractions || section == DeckSection.Contraptions)
|
||||
return Integer.MAX_VALUE;
|
||||
return super.getExtraSectionMaxCopies(section);
|
||||
}
|
||||
},
|
||||
Commander ( Range.is(99), Range.of(0, 10), 1, null,
|
||||
card -> StaticData.instance().getCommanderPredicate().test(card)
|
||||
@@ -112,13 +108,7 @@ public enum DeckFormat {
|
||||
}
|
||||
},
|
||||
PlanarConquest ( Range.of(40, Integer.MAX_VALUE), Range.is(0), 1),
|
||||
Adventure ( Range.of(40, Integer.MAX_VALUE), Range.of(0, Integer.MAX_VALUE), 4) {
|
||||
@Override
|
||||
public boolean allowCustomCards() {
|
||||
//If the player has them, may as well allow them.
|
||||
return true;
|
||||
}
|
||||
},
|
||||
Adventure ( Range.of(40, Integer.MAX_VALUE), Range.of(0, 15), 4),
|
||||
Vanguard ( Range.of(60, Integer.MAX_VALUE), Range.is(0), 4),
|
||||
Planechase ( Range.of(60, Integer.MAX_VALUE), Range.is(0), 4),
|
||||
Archenemy ( Range.of(60, Integer.MAX_VALUE), Range.is(0), 4),
|
||||
@@ -201,57 +191,12 @@ public enum DeckFormat {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the default maximum copies of a card in this format.
|
||||
* @return the maxCardCopies
|
||||
*/
|
||||
public int getMaxCardCopies() {
|
||||
return maxCardCopies;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the maximum copies of the specified card allowed in this format. This does not include ban or restricted lists.
|
||||
*/
|
||||
public int getMaxCardCopies(PaperCard card) {
|
||||
if(canHaveSpecificNumberInDeck(card) != null)
|
||||
return canHaveSpecificNumberInDeck(card);
|
||||
else if (canHaveAnyNumberOf(card))
|
||||
return Integer.MAX_VALUE;
|
||||
else if (card.getRules().isVariant()) {
|
||||
DeckSection section = DeckSection.matchingSection(card);
|
||||
if(section == DeckSection.Planes && card.getRules().getType().isPhenomenon())
|
||||
return 2; //These are two-of.
|
||||
return getExtraSectionMaxCopies(section);
|
||||
}
|
||||
else
|
||||
return this.getMaxCardCopies();
|
||||
}
|
||||
|
||||
public int getExtraSectionMaxCopies(DeckSection section) {
|
||||
return switch (section) {
|
||||
case Avatar, Commander, Planes, Dungeon, Attractions, Contraptions -> 1;
|
||||
case Schemes -> 2;
|
||||
case Conspiracy -> Integer.MAX_VALUE;
|
||||
default -> maxCardCopies;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the deck sections used by most decks in this format.
|
||||
*/
|
||||
public EnumSet<DeckSection> getPrimaryDeckSections() {
|
||||
if(this == Planechase)
|
||||
return EnumSet.of(DeckSection.Planes);
|
||||
if(this == Archenemy)
|
||||
return EnumSet.of(DeckSection.Schemes);
|
||||
if(this == Vanguard)
|
||||
return EnumSet.of(DeckSection.Avatar);
|
||||
EnumSet<DeckSection> out = EnumSet.of(DeckSection.Main);
|
||||
if(sideRange == null || sideRange.getMaximum() > 0)
|
||||
out.add(DeckSection.Sideboard);
|
||||
if(hasCommander())
|
||||
out.add(DeckSection.Commander);
|
||||
return out;
|
||||
}
|
||||
|
||||
public String getDeckConformanceProblem(Deck deck) {
|
||||
if (deck == null) {
|
||||
return "is not selected";
|
||||
@@ -408,7 +353,7 @@ public enum DeckFormat {
|
||||
// Should group all cards by name, so that different editions of same card are really counted as the same card
|
||||
for (final Entry<String, Integer> cp : Aggregates.groupSumBy(allCards, pc -> StaticData.instance().getCommonCards().getName(pc.getName(), true))) {
|
||||
IPaperCard simpleCard = StaticData.instance().getCommonCards().getCard(cp.getKey());
|
||||
if (simpleCard != null && simpleCard.getRules().isCustom() && !allowCustomCards())
|
||||
if (simpleCard != null && simpleCard.getRules().isCustom() && !StaticData.instance().allowCustomCardsInDecksConformance())
|
||||
return TextUtil.concatWithSpace("contains a Custom Card:", cp.getKey(), "\nPlease Enable Custom Cards in Forge Preferences to use this deck.");
|
||||
// Might cause issues since it ignores "Special" Cards
|
||||
if (simpleCard == null) {
|
||||
@@ -539,10 +484,6 @@ public enum DeckFormat {
|
||||
// Not needed by default
|
||||
}
|
||||
|
||||
public boolean allowCustomCards() {
|
||||
return StaticData.instance().allowCustomCardsInDecksConformance();
|
||||
}
|
||||
|
||||
public boolean isLegalCard(PaperCard pc) {
|
||||
if (cardPoolFilter == null) {
|
||||
if (paperCardPoolFilter == null) {
|
||||
@@ -557,13 +498,13 @@ public enum DeckFormat {
|
||||
if (cardPoolFilter != null && !cardPoolFilter.test(rules)) {
|
||||
return false;
|
||||
}
|
||||
if (this == DeckFormat.Oathbreaker) {
|
||||
if (this.equals(DeckFormat.Oathbreaker)) {
|
||||
return rules.canBeOathbreaker();
|
||||
}
|
||||
if (this == DeckFormat.Brawl) {
|
||||
if (this.equals(DeckFormat.Brawl)) {
|
||||
return rules.canBeBrawlCommander();
|
||||
}
|
||||
if (this == DeckFormat.TinyLeaders) {
|
||||
if (this.equals(DeckFormat.TinyLeaders)) {
|
||||
return rules.canBeTinyLeadersCommander();
|
||||
}
|
||||
return rules.canBeCommander();
|
||||
@@ -612,8 +553,6 @@ public enum DeckFormat {
|
||||
for (final PaperCard p : commanders) {
|
||||
cmdCI |= p.getRules().getColorIdentity().getColor();
|
||||
}
|
||||
if(cmdCI == MagicColor.ALL_COLORS)
|
||||
return x -> true;
|
||||
Predicate<CardRules> predicate = CardRulesPredicates.hasColorIdentity(cmdCI);
|
||||
if (commanders.size() == 1 && commanders.get(0).getRules().canBePartnerCommander()) {
|
||||
// Also show available partners a commander can have a partner.
|
||||
|
||||
@@ -49,16 +49,6 @@ public class DeckRecognizer {
|
||||
LIMITED_CARD,
|
||||
CARD_FROM_NOT_ALLOWED_SET,
|
||||
CARD_FROM_INVALID_SET,
|
||||
/**
|
||||
* Valid card request, but can't be imported because the player does not have enough copies.
|
||||
* Should be replaced with a different printing if possible.
|
||||
*/
|
||||
CARD_NOT_IN_INVENTORY,
|
||||
/**
|
||||
* Valid card request for a card that isn't in the player's inventory, but new copies can be acquired freely.
|
||||
* Usually used for basic lands. Should be supplied to the import controller by the editor.
|
||||
*/
|
||||
FREE_CARD_NOT_IN_INVENTORY,
|
||||
// Warning messages
|
||||
WARNING_MESSAGE,
|
||||
UNKNOWN_CARD,
|
||||
@@ -73,14 +63,10 @@ public class DeckRecognizer {
|
||||
CARD_TYPE,
|
||||
CARD_RARITY,
|
||||
CARD_CMC,
|
||||
MANA_COLOUR;
|
||||
|
||||
public static final EnumSet<TokenType> CARD_TOKEN_TYPES = EnumSet.of(LEGAL_CARD, LIMITED_CARD, CARD_FROM_NOT_ALLOWED_SET, CARD_FROM_INVALID_SET, CARD_NOT_IN_INVENTORY, FREE_CARD_NOT_IN_INVENTORY);
|
||||
public static final EnumSet<TokenType> IN_DECK_TOKEN_TYPES = EnumSet.of(LEGAL_CARD, LIMITED_CARD, DECK_NAME, FREE_CARD_NOT_IN_INVENTORY);
|
||||
public static final EnumSet<TokenType> CARD_PLACEHOLDER_TOKEN_TYPES = EnumSet.of(CARD_TYPE, CARD_RARITY, CARD_CMC, MANA_COLOUR);
|
||||
MANA_COLOUR
|
||||
}
|
||||
|
||||
public enum LimitedCardType {
|
||||
public enum LimitedCardType{
|
||||
BANNED,
|
||||
RESTRICTED,
|
||||
}
|
||||
@@ -122,10 +108,6 @@ public class DeckRecognizer {
|
||||
return new Token(TokenType.CARD_FROM_INVALID_SET, count, card, cardRequestHasSetCode);
|
||||
}
|
||||
|
||||
public static Token NotInInventoryFree(final PaperCard card, final int count, final DeckSection section) {
|
||||
return new Token(TokenType.FREE_CARD_NOT_IN_INVENTORY, count, card, section, true);
|
||||
}
|
||||
|
||||
// WARNING MESSAGES
|
||||
// ================
|
||||
public static Token UnknownCard(final String cardName, final String setCode, final int count) {
|
||||
@@ -144,10 +126,6 @@ public class DeckRecognizer {
|
||||
return new Token(TokenType.WARNING_MESSAGE, msg);
|
||||
}
|
||||
|
||||
public static Token NotInInventory(final PaperCard card, final int count, final DeckSection section) {
|
||||
return new Token(TokenType.CARD_NOT_IN_INVENTORY, count, card, section, false);
|
||||
}
|
||||
|
||||
/* =================================
|
||||
* DECK SECTIONS
|
||||
* ================================= */
|
||||
@@ -261,11 +239,14 @@ public class DeckRecognizer {
|
||||
/**
|
||||
* Filters all token types that have a PaperCard instance set (not null)
|
||||
* @return true for tokens of type:
|
||||
* LEGAL_CARD, LIMITED_CARD, CARD_FROM_NOT_ALLOWED_SET and CARD_FROM_INVALID_SET, CARD_NOT_IN_INVENTORY, FREE_CARD_NOT_IN_INVENTORY.
|
||||
* LEGAL_CARD, LIMITED_CARD, CARD_FROM_NOT_ALLOWED_SET and CARD_FROM_INVALID_SET.
|
||||
* False otherwise.
|
||||
*/
|
||||
public boolean isCardToken() {
|
||||
return TokenType.CARD_TOKEN_TYPES.contains(this.type);
|
||||
return (this.type == TokenType.LEGAL_CARD ||
|
||||
this.type == TokenType.LIMITED_CARD ||
|
||||
this.type == TokenType.CARD_FROM_NOT_ALLOWED_SET ||
|
||||
this.type == TokenType.CARD_FROM_INVALID_SET);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,7 +255,9 @@ public class DeckRecognizer {
|
||||
* LEGAL_CARD, LIMITED_CARD, DECK_NAME; false otherwise.
|
||||
*/
|
||||
public boolean isTokenForDeck() {
|
||||
return TokenType.IN_DECK_TOKEN_TYPES.contains(this.type);
|
||||
return (this.type == TokenType.LEGAL_CARD ||
|
||||
this.type == TokenType.LIMITED_CARD ||
|
||||
this.type == TokenType.DECK_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,7 +266,7 @@ public class DeckRecognizer {
|
||||
* False otherwise.
|
||||
*/
|
||||
public boolean isCardTokenForDeck() {
|
||||
return isCardToken() && isTokenForDeck();
|
||||
return (this.type == TokenType.LEGAL_CARD || this.type == TokenType.LIMITED_CARD);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -293,7 +276,10 @@ public class DeckRecognizer {
|
||||
* CARD_RARITY, CARD_CMC, CARD_TYPE, MANA_COLOUR
|
||||
*/
|
||||
public boolean isCardPlaceholder(){
|
||||
return TokenType.CARD_PLACEHOLDER_TOKEN_TYPES.contains(this.type);
|
||||
return (this.type == TokenType.CARD_RARITY ||
|
||||
this.type == TokenType.CARD_CMC ||
|
||||
this.type == TokenType.MANA_COLOUR ||
|
||||
this.type == TokenType.CARD_TYPE);
|
||||
}
|
||||
|
||||
/** Determines if current token is a Deck Section token
|
||||
@@ -550,7 +536,7 @@ public class DeckRecognizer {
|
||||
PaperCard tokenCard = token.getCard();
|
||||
|
||||
if (isAllowed(tokenSection)) {
|
||||
if (tokenSection != referenceDeckSectionInParsing) {
|
||||
if (!tokenSection.equals(referenceDeckSectionInParsing)) {
|
||||
Token sectionToken = Token.DeckSection(tokenSection.name(), this.allowedDeckSections);
|
||||
// just check that last token is stack is a card placeholder.
|
||||
// In that case, add the new section token before the placeholder
|
||||
@@ -589,7 +575,7 @@ public class DeckRecognizer {
|
||||
refLine = purgeAllLinks(refLine);
|
||||
|
||||
String line;
|
||||
if (refLine.startsWith(LINE_COMMENT_DELIMITER_OR_MD_HEADER))
|
||||
if (StringUtils.startsWith(refLine, LINE_COMMENT_DELIMITER_OR_MD_HEADER))
|
||||
line = refLine.replaceAll(LINE_COMMENT_DELIMITER_OR_MD_HEADER, "");
|
||||
else
|
||||
line = refLine.trim(); // Remove any trailing formatting
|
||||
@@ -598,7 +584,7 @@ public class DeckRecognizer {
|
||||
// Final fantasy cards like Summon: Choco/Mog should be ommited to be recognized. TODO: fix maybe for future cards
|
||||
if (!line.contains("Summon:"))
|
||||
line = SEARCH_SINGLE_SLASH.matcher(line).replaceFirst(" // ");
|
||||
if (line.startsWith(ASTERISK)) // Markdown lists (tappedout md export)
|
||||
if (StringUtils.startsWith(line, ASTERISK)) // markdown lists (tappedout md export)
|
||||
line = line.substring(2);
|
||||
|
||||
// == Patches to Corner Cases
|
||||
@@ -614,8 +600,8 @@ public class DeckRecognizer {
|
||||
Token result = recogniseCardToken(line, referenceSection);
|
||||
if (result == null)
|
||||
result = recogniseNonCardToken(line);
|
||||
return result != null ? result : refLine.startsWith(DOUBLE_SLASH) ||
|
||||
refLine.startsWith(LINE_COMMENT_DELIMITER_OR_MD_HEADER) ?
|
||||
return result != null ? result : StringUtils.startsWith(refLine, DOUBLE_SLASH) ||
|
||||
StringUtils.startsWith(refLine, LINE_COMMENT_DELIMITER_OR_MD_HEADER) ?
|
||||
new Token(TokenType.COMMENT, 0, refLine) : new Token(TokenType.UNKNOWN_TEXT, 0, refLine);
|
||||
}
|
||||
|
||||
@@ -627,7 +613,7 @@ public class DeckRecognizer {
|
||||
while (m.find()) {
|
||||
line = line.replaceAll(m.group(), "").trim();
|
||||
}
|
||||
if (line.endsWith("()"))
|
||||
if (StringUtils.endsWith(line, "()"))
|
||||
return line.substring(0, line.length()-2);
|
||||
return line;
|
||||
}
|
||||
@@ -755,12 +741,21 @@ public class DeckRecognizer {
|
||||
// This would save tons of time in parsing Input + would also allow to return UnsupportedCardTokens beforehand
|
||||
private DeckSection getTokenSection(String deckSec, DeckSection currentDeckSection, PaperCard card){
|
||||
if (deckSec != null) {
|
||||
DeckSection cardSection = switch (deckSec.toUpperCase().trim()) {
|
||||
case "MB" -> DeckSection.Main;
|
||||
case "SB" -> DeckSection.Sideboard;
|
||||
case "CM" -> DeckSection.Commander;
|
||||
default -> DeckSection.matchingSection(card);
|
||||
};
|
||||
DeckSection cardSection;
|
||||
switch (deckSec.toUpperCase().trim()) {
|
||||
case "MB":
|
||||
cardSection = DeckSection.Main;
|
||||
break;
|
||||
case "SB":
|
||||
cardSection = DeckSection.Sideboard;
|
||||
break;
|
||||
case "CM":
|
||||
cardSection = DeckSection.Commander;
|
||||
break;
|
||||
default:
|
||||
cardSection = DeckSection.matchingSection(card);
|
||||
break;
|
||||
}
|
||||
if (cardSection.validate(card))
|
||||
return cardSection;
|
||||
}
|
||||
@@ -994,7 +989,7 @@ public class DeckRecognizer {
|
||||
private static String getMagicColourLabel(MagicColor.Color magicColor) {
|
||||
if (magicColor == null) // Multicolour
|
||||
return String.format("%s {W}{U}{B}{R}{G}", getLocalisedMagicColorName("Multicolour"));
|
||||
return String.format("%s %s", magicColor.getTranslatedName(), magicColor.getSymbol());
|
||||
return String.format("%s %s", magicColor.getLocalizedName(), magicColor.getSymbol());
|
||||
}
|
||||
|
||||
private static final HashMap<Integer, String> manaSymbolsMap = new HashMap<Integer, String>() {{
|
||||
@@ -1013,30 +1008,60 @@ public class DeckRecognizer {
|
||||
if (magicColor2 == null || magicColor2 == MagicColor.Color.COLORLESS
|
||||
|| magicColor1 == MagicColor.Color.COLORLESS)
|
||||
return String.format("%s // %s", getMagicColourLabel(magicColor1), getMagicColourLabel(magicColor2));
|
||||
String localisedName1 = magicColor1.getTranslatedName();
|
||||
String localisedName2 = magicColor2.getTranslatedName();
|
||||
String comboManaSymbol = manaSymbolsMap.get(magicColor1.getColorMask() | magicColor2.getColorMask());
|
||||
String localisedName1 = magicColor1.getLocalizedName();
|
||||
String localisedName2 = magicColor2.getLocalizedName();
|
||||
String comboManaSymbol = manaSymbolsMap.get(magicColor1.getColormask() | magicColor2.getColormask());
|
||||
return String.format("%s/%s {%s}", localisedName1, localisedName2, comboManaSymbol);
|
||||
}
|
||||
|
||||
private static MagicColor.Color getMagicColor(String colorName){
|
||||
if (colorName.toLowerCase().startsWith("multi") || colorName.equalsIgnoreCase("m"))
|
||||
return null; // will be handled separately
|
||||
return MagicColor.Color.fromByte(MagicColor.fromName(colorName.toLowerCase()));
|
||||
|
||||
byte color = MagicColor.fromName(colorName.toLowerCase());
|
||||
switch (color) {
|
||||
case MagicColor.WHITE:
|
||||
return MagicColor.Color.WHITE;
|
||||
case MagicColor.BLUE:
|
||||
return MagicColor.Color.BLUE;
|
||||
case MagicColor.BLACK:
|
||||
return MagicColor.Color.BLACK;
|
||||
case MagicColor.RED:
|
||||
return MagicColor.Color.RED;
|
||||
case MagicColor.GREEN:
|
||||
return MagicColor.Color.GREEN;
|
||||
default:
|
||||
return MagicColor.Color.COLORLESS;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static String getLocalisedMagicColorName(String colorName){
|
||||
Localizer localizer = Localizer.getInstance();
|
||||
return switch (colorName.toLowerCase()) {
|
||||
case MagicColor.Constant.WHITE -> localizer.getMessage("lblWhite");
|
||||
case MagicColor.Constant.BLUE -> localizer.getMessage("lblBlue");
|
||||
case MagicColor.Constant.BLACK -> localizer.getMessage("lblBlack");
|
||||
case MagicColor.Constant.RED -> localizer.getMessage("lblRed");
|
||||
case MagicColor.Constant.GREEN -> localizer.getMessage("lblGreen");
|
||||
case MagicColor.Constant.COLORLESS -> localizer.getMessage("lblColorless");
|
||||
case "multicolour", "multicolor" -> localizer.getMessage("lblMulticolor");
|
||||
default -> "";
|
||||
};
|
||||
switch(colorName.toLowerCase()){
|
||||
case MagicColor.Constant.WHITE:
|
||||
return localizer.getMessage("lblWhite");
|
||||
|
||||
case MagicColor.Constant.BLUE:
|
||||
return localizer.getMessage("lblBlue");
|
||||
|
||||
case MagicColor.Constant.BLACK:
|
||||
return localizer.getMessage("lblBlack");
|
||||
|
||||
case MagicColor.Constant.RED:
|
||||
return localizer.getMessage("lblRed");
|
||||
|
||||
case MagicColor.Constant.GREEN:
|
||||
return localizer.getMessage("lblGreen");
|
||||
|
||||
case MagicColor.Constant.COLORLESS:
|
||||
return localizer.getMessage("lblColorless");
|
||||
case "multicolour":
|
||||
case "multicolor":
|
||||
return localizer.getMessage("lblMulticolor");
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1055,6 +1080,37 @@ public class DeckRecognizer {
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
private static Pair<String, String> getManaNameAndSymbol(String matchedMana) {
|
||||
if (matchedMana == null)
|
||||
return null;
|
||||
|
||||
Localizer localizer = Localizer.getInstance();
|
||||
switch (matchedMana.toLowerCase()) {
|
||||
case MagicColor.Constant.WHITE:
|
||||
case "w":
|
||||
return Pair.of(localizer.getMessage("lblWhite"), MagicColor.Color.WHITE.getSymbol());
|
||||
case MagicColor.Constant.BLUE:
|
||||
case "u":
|
||||
return Pair.of(localizer.getMessage("lblBlue"), MagicColor.Color.BLUE.getSymbol());
|
||||
case MagicColor.Constant.BLACK:
|
||||
case "b":
|
||||
return Pair.of(localizer.getMessage("lblBlack"), MagicColor.Color.BLACK.getSymbol());
|
||||
case MagicColor.Constant.RED:
|
||||
case "r":
|
||||
return Pair.of(localizer.getMessage("lblRed"), MagicColor.Color.RED.getSymbol());
|
||||
case MagicColor.Constant.GREEN:
|
||||
case "g":
|
||||
return Pair.of(localizer.getMessage("lblGreen"), MagicColor.Color.GREEN.getSymbol());
|
||||
case MagicColor.Constant.COLORLESS:
|
||||
case "c":
|
||||
return Pair.of(localizer.getMessage("lblColorless"), MagicColor.Color.COLORLESS.getSymbol());
|
||||
default: // Multicolour
|
||||
return Pair.of(localizer.getMessage("lblMulticolor"), "");
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isDeckName(final String lineAsIs) {
|
||||
if (lineAsIs == null)
|
||||
return false;
|
||||
|
||||
@@ -52,4 +52,9 @@ public interface IPaperCard extends InventoryItem, Serializable {
|
||||
default String getUntranslatedType() {
|
||||
return getRules().getType().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
default String getUntranslatedOracle() {
|
||||
return getRules().getOracleText();
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
|
||||
// These fields are kinda PK for PrintedCard
|
||||
private final String name;
|
||||
private String edition;
|
||||
private final String edition;
|
||||
/* [NEW] Attribute to store reference to CollectorNumber of each PaperCard.
|
||||
By default the attribute is marked as "unset" so that it could be retrieved and set.
|
||||
(see getCollectorNumber())
|
||||
@@ -154,31 +154,6 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
return this.noSellVersion;
|
||||
}
|
||||
|
||||
public PaperCard getMeldBaseCard() {
|
||||
if (getRules().getSplitType() != CardSplitType.Meld) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// This is the base part of the meld duo
|
||||
if (getRules().getOtherPart() == null) {
|
||||
return this;
|
||||
}
|
||||
|
||||
String meldWith = getRules().getMeldWith();
|
||||
if (meldWith == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<PrintSheet> sheets = StaticData.instance().getCardEdition(this.edition).getPrintSheetsBySection();
|
||||
for (PrintSheet sheet : sheets) {
|
||||
if (sheet.contains(this)) {
|
||||
return sheet.find(PaperCardPredicates.name(meldWith));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public PaperCard copyWithoutFlags() {
|
||||
if(this.flaglessVersion == null) {
|
||||
if(this.flags == PaperCardFlags.IDENTITY_FLAGS)
|
||||
@@ -250,7 +225,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
this.artIndex = Math.max(artIndex, IPaperCard.DEFAULT_ART_INDEX);
|
||||
this.foil = foil;
|
||||
this.rarity = rarity;
|
||||
this.artist = artist;
|
||||
this.artist = TextUtil.normalizeText(artist);
|
||||
this.collectorNumber = (collectorNumber != null && !collectorNumber.isEmpty()) ? collectorNumber : IPaperCard.NO_COLLECTOR_NUMBER;
|
||||
// If the user changes the language this will make cards sort by the old language until they restart the game.
|
||||
// This is a good tradeoff
|
||||
@@ -375,8 +350,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
System.out.println("PaperCard: " + name + " not found with set and index " + edition + ", " + artIndex);
|
||||
pc = readObjectAlternate(name, edition);
|
||||
if (pc == null) {
|
||||
pc = StaticData.instance().getCommonCards().createUnsupportedCard(name);
|
||||
//throw new IOException(TextUtil.concatWithSpace("Card", name, "not found with set and index", edition, Integer.toString(artIndex)));
|
||||
throw new IOException(TextUtil.concatWithSpace("Card", name, "not found with set and index", edition, Integer.toString(artIndex)));
|
||||
}
|
||||
System.out.println("Alternate object found: " + pc.getName() + ", " + pc.getEdition() + ", " + pc.getArtIndex());
|
||||
}
|
||||
@@ -593,7 +567,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
|
||||
public PaperCardFlags withMarkedColors(ColorSet markedColors) {
|
||||
if(markedColors == null)
|
||||
markedColors = ColorSet.NO_COLORS;
|
||||
markedColors = ColorSet.getNullColor();
|
||||
return new PaperCardFlags(this, markedColors, null);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,13 +50,6 @@ public abstract class PaperCardPredicates {
|
||||
return new PredicateNames(what);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters on a card foil status
|
||||
*/
|
||||
public static Predicate<PaperCard> isFoil(final boolean isFoil) {
|
||||
return new PredicateFoil(isFoil);
|
||||
}
|
||||
|
||||
private static final class PredicatePrintedWithRarity implements Predicate<PaperCard> {
|
||||
private final CardRarity matchingRarity;
|
||||
|
||||
@@ -100,17 +93,6 @@ public abstract class PaperCardPredicates {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PredicateFoil implements Predicate<PaperCard> {
|
||||
private final boolean operand;
|
||||
|
||||
@Override
|
||||
public boolean test(final PaperCard card) { return card.isFoil() == operand; }
|
||||
|
||||
private PredicateFoil(final boolean isFoil) {
|
||||
this.operand = isFoil;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PredicateRarity implements Predicate<PaperCard> {
|
||||
private final CardRarity operand;
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
|
||||
return false;
|
||||
CardSplitType cst = this.cardRules.getSplitType();
|
||||
//expand this on future for other tokens that has other backsides besides transform..
|
||||
return cst == CardSplitType.Transform || cst == CardSplitType.Modal;
|
||||
return cst == CardSplitType.Transform;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -19,11 +19,6 @@ public class SealedTemplate {
|
||||
Pair.of(BoosterSlots.RARE_MYTHIC, 1), Pair.of(BoosterSlots.BASIC_LAND, 1)
|
||||
));
|
||||
|
||||
// This is a generic cube booster. 15 cards, no rarity slots.
|
||||
public final static SealedTemplate genericNoSlotBooster = new SealedTemplate(null, Lists.newArrayList(
|
||||
Pair.of(BoosterSlots.ANY, 15)
|
||||
));
|
||||
|
||||
protected final List<Pair<String, Integer>> slots;
|
||||
|
||||
protected final String name;
|
||||
|
||||
@@ -254,7 +254,7 @@ public class BoosterGenerator {
|
||||
|
||||
if (sheetKey.startsWith("wholeSheet")) {
|
||||
PrintSheet ps = getPrintSheet(sheetKey);
|
||||
result.addAll(ps.toFlatList());
|
||||
result.addAll(ps.all());
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -384,7 +384,7 @@ public class BoosterGenerator {
|
||||
PrintSheet replaceThis = tryGetStaticSheet(split[0]);
|
||||
List<PaperCard> candidates = Lists.newArrayList();
|
||||
for (PaperCard p : result) {
|
||||
if (replaceThis.contains(p)) {
|
||||
if (replaceThis.all().contains(p)) {
|
||||
candidates.add(candidates.size(), p);
|
||||
}
|
||||
}
|
||||
@@ -398,7 +398,7 @@ public class BoosterGenerator {
|
||||
PrintSheet replaceThis = tryGetStaticSheet(split[0]);
|
||||
List<PaperCard> candidates = Lists.newArrayList();
|
||||
for (PaperCard p : result) {
|
||||
if (replaceThis.contains(p)) {
|
||||
if (replaceThis.all().contains(p)) {
|
||||
candidates.add(candidates.size(), p);
|
||||
}
|
||||
}
|
||||
@@ -462,8 +462,9 @@ public class BoosterGenerator {
|
||||
} else {
|
||||
paperCards.addAll(ps.random(numCardsToGenerate, true));
|
||||
}
|
||||
|
||||
result.addAll(paperCards);
|
||||
}
|
||||
result.addAll(paperCards);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -633,10 +634,7 @@ public class BoosterGenerator {
|
||||
System.out.println("Parsing from main code: " + mainCode);
|
||||
String sheetName = StringUtils.strip(mainCode.substring(10), "()\" ");
|
||||
System.out.println("Attempting to lookup: " + sheetName);
|
||||
PrintSheet fromSheet = tryGetStaticSheet(sheetName);
|
||||
if (fromSheet == null)
|
||||
throw new RuntimeException("PrintSheet Error: " + ps.getName() + " didn't find " + sheetName + " from " + mainCode);
|
||||
src = fromSheet.toFlatList();
|
||||
src = tryGetStaticSheet(sheetName).toFlatList();
|
||||
setPred = x -> true;
|
||||
|
||||
} else if (mainCode.startsWith("promo") || mainCode.startsWith("name")) { // get exactly the named cards, that's a tiny inlined print sheet
|
||||
|
||||
@@ -10,11 +10,13 @@ public interface ITranslatable extends IHasName {
|
||||
default String getUntranslatedName() {
|
||||
return getName();
|
||||
}
|
||||
default String getTranslatedName() {
|
||||
return getName();
|
||||
}
|
||||
|
||||
default String getUntranslatedType() {
|
||||
return "";
|
||||
}
|
||||
|
||||
default String getUntranslatedOracle() {
|
||||
return "";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -199,14 +199,19 @@ public class ImageUtil {
|
||||
return getImageRelativePath(cp, face, true, true);
|
||||
}
|
||||
|
||||
|
||||
public static String getScryfallDownloadUrl(PaperCard cp, String face, String setCode, String langCode, boolean useArtCrop){
|
||||
return getScryfallDownloadUrl(cp, face, setCode, langCode, useArtCrop, false);
|
||||
}
|
||||
|
||||
public static String getScryfallDownloadUrl(PaperCard cp, String face, String setCode, String langCode, boolean useArtCrop, boolean hyphenateAlchemy){
|
||||
String editionCode;
|
||||
if (setCode != null && !setCode.isEmpty())
|
||||
editionCode = setCode;
|
||||
else
|
||||
editionCode = cp.getEdition().toLowerCase();
|
||||
String cardCollectorNumber = cp.getCollectorNumber();
|
||||
// Hack to account for variations in Arabian Nights
|
||||
cardCollectorNumber = cardCollectorNumber.replace("+", "†");
|
||||
// override old planechase sets from their modified id since scryfall move the planechase cards outside their original setcode
|
||||
if (cardCollectorNumber.startsWith("OHOP")) {
|
||||
editionCode = "ohop";
|
||||
@@ -217,42 +222,29 @@ public class ImageUtil {
|
||||
} else if (cardCollectorNumber.startsWith("OPC2")) {
|
||||
editionCode = "opc2";
|
||||
cardCollectorNumber = cardCollectorNumber.substring("OPC2".length());
|
||||
} else if (hyphenateAlchemy) {
|
||||
if (!cardCollectorNumber.startsWith("A")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
cardCollectorNumber = cardCollectorNumber.replace("A", "A-");
|
||||
}
|
||||
String versionParam = useArtCrop ? "art_crop" : "normal";
|
||||
String faceParam = "";
|
||||
if (cp.getRules().getOtherPart() != null) {
|
||||
faceParam = (face.equals("back") ? "&face=back" : "&face=front");
|
||||
} else if (cp.getRules().getSplitType() == CardSplitType.Meld
|
||||
&& !cardCollectorNumber.endsWith("a")
|
||||
&& !cardCollectorNumber.endsWith("b")) {
|
||||
|
||||
if (cp.getRules().getSplitType() == CardSplitType.Meld) {
|
||||
if (face.equals("back")) {
|
||||
PaperCard meldBasePc = cp.getMeldBaseCard();
|
||||
cardCollectorNumber = meldBasePc.getCollectorNumber();
|
||||
String collectorNumberSuffix = "";
|
||||
|
||||
if (cardCollectorNumber.endsWith("a")) {
|
||||
cardCollectorNumber = cardCollectorNumber.substring(0, cardCollectorNumber.length() - 1);
|
||||
} else if (cardCollectorNumber.endsWith("as")) {
|
||||
cardCollectorNumber = cardCollectorNumber.substring(0, cardCollectorNumber.length() - 2);
|
||||
collectorNumberSuffix = "s";
|
||||
} else if (cardCollectorNumber.endsWith("ap")) {
|
||||
cardCollectorNumber = cardCollectorNumber.substring(0, cardCollectorNumber.length() - 2);
|
||||
collectorNumberSuffix = "p";
|
||||
} else if (cp.getCollectorNumber().endsWith("a")) {
|
||||
// SIR
|
||||
cardCollectorNumber = cp.getCollectorNumber().substring(0, cp.getCollectorNumber().length() - 1);
|
||||
// Only the bottom half of a meld card shares a collector number.
|
||||
// Hanweir Garrison EMN already has a appended.
|
||||
// Exception: The front facing card doesn't use a in FIN
|
||||
if (face.equals("back")) {
|
||||
cardCollectorNumber += "b";
|
||||
} else if (!editionCode.equals("fin")) {
|
||||
cardCollectorNumber += "a";
|
||||
}
|
||||
|
||||
cardCollectorNumber += "b" + collectorNumberSuffix;
|
||||
}
|
||||
|
||||
faceParam = "&face=front";
|
||||
} else if (cp.getRules().getOtherPart() != null) {
|
||||
faceParam = (face.equals("back") && cp.getRules().getSplitType() != CardSplitType.Flip
|
||||
? "&face=back"
|
||||
: "&face=front");
|
||||
}
|
||||
|
||||
if (cardCollectorNumber.endsWith("☇")) {
|
||||
faceParam = "&face=back";
|
||||
cardCollectorNumber = cardCollectorNumber.substring(0, cardCollectorNumber.length() - 1);
|
||||
}
|
||||
|
||||
return String.format("%s/%s/%s?format=image&version=%s%s", editionCode, encodeUtf8(cardCollectorNumber),
|
||||
@@ -264,10 +256,6 @@ public class ImageUtil {
|
||||
if (!faceParam.isEmpty()) {
|
||||
faceParam = (faceParam.equals("back") ? "&face=back" : "&face=front");
|
||||
}
|
||||
if (collectorNumber.endsWith("☇")) {
|
||||
faceParam = "&face=back";
|
||||
collectorNumber = collectorNumber.substring(0, collectorNumber.length() - 1);
|
||||
}
|
||||
return String.format("%s/%s/%s?format=image&version=%s%s", setCode, encodeUtf8(collectorNumber),
|
||||
langCode, versionParam, faceParam);
|
||||
}
|
||||
@@ -288,7 +276,8 @@ public class ImageUtil {
|
||||
char c;
|
||||
for (int i = 0; i < in.length(); i++) {
|
||||
c = in.charAt(i);
|
||||
if ((c != '"') && (c != '/') && (c != ':') && (c != '?')) {
|
||||
if ((c == '"') || (c == '/') || (c == ':') || (c == '?')) {
|
||||
} else {
|
||||
out.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,16 +269,11 @@ public class ItemPool<T extends InventoryItem> implements Iterable<Entry<T, Inte
|
||||
// need not set out-of-sync: either remove did set, or nothing was removed
|
||||
}
|
||||
|
||||
public void removeIf(Predicate<T> filter) {
|
||||
items.keySet().removeIf(filter);
|
||||
}
|
||||
|
||||
public void retainIf(Predicate<T> filter) {
|
||||
items.keySet().removeIf(filter.negate());
|
||||
}
|
||||
|
||||
public T find(Predicate<T> filter) {
|
||||
return items.keySet().stream().filter(filter).findFirst().orElse(null);
|
||||
public void removeIf(Predicate<T> test) {
|
||||
for (final T item : items.keySet()) {
|
||||
if (test.test(item))
|
||||
remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
@@ -290,19 +285,4 @@ public class ItemPool<T extends InventoryItem> implements Iterable<Entry<T, Inte
|
||||
return (obj instanceof ItemPool ip) &&
|
||||
(this.items.equals(ip.items));
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a predicate to this ItemPool's entries.
|
||||
*
|
||||
* @param predicate the Predicate to apply to this ItemPool
|
||||
* @return a new ItemPool made from this ItemPool with only the items that agree with the provided Predicate
|
||||
*/
|
||||
public ItemPool<T> getFilteredPool(Predicate<T> predicate) {
|
||||
ItemPool<T> filteredPool = new ItemPool<>(myClass);
|
||||
for (T c : this.items.keySet()) {
|
||||
if (predicate.test(c))
|
||||
filteredPool.add(c, this.items.get(c));
|
||||
}
|
||||
return filteredPool;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<dependency>
|
||||
<groupId>io.sentry</groupId>
|
||||
<artifactId>sentry-logback</artifactId>
|
||||
<version>8.21.1</version>
|
||||
<version>7.15.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jgrapht</groupId>
|
||||
|
||||
@@ -62,9 +62,7 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
|
||||
|
||||
/** Keys of descriptive (text) parameters. */
|
||||
private static final ImmutableList<String> descriptiveKeys = ImmutableList.<String>builder()
|
||||
.add("Description", "SpellDescription", "StackDescription", "TriggerDescription")
|
||||
.add("ChangeTypeDesc")
|
||||
.build();
|
||||
.add("Description", "SpellDescription", "StackDescription", "TriggerDescription").build();
|
||||
|
||||
/**
|
||||
* Keys that should not changed
|
||||
|
||||
@@ -35,7 +35,7 @@ public class ForgeScript {
|
||||
boolean withSource = property.endsWith("Source");
|
||||
final ColorSet colors;
|
||||
if (withSource && StaticAbilityColorlessDamageSource.colorlessDamageSource(cardState)) {
|
||||
colors = ColorSet.NO_COLORS;
|
||||
colors = ColorSet.getNullColor();
|
||||
} else {
|
||||
colors = cardState.getCard().getColor(cardState);
|
||||
}
|
||||
@@ -166,6 +166,8 @@ public class ForgeScript {
|
||||
Card source, CardTraitBase spellAbility) {
|
||||
if (property.equals("ManaAbility")) {
|
||||
return sa.isManaAbility();
|
||||
} else if (property.equals("nonManaAbility")) {
|
||||
return !sa.isManaAbility();
|
||||
} else if (property.equals("withoutXCost")) {
|
||||
return !sa.costHasManaX();
|
||||
} else if (property.startsWith("XCost")) {
|
||||
@@ -193,7 +195,7 @@ public class ForgeScript {
|
||||
return sa.isKeyword(Keyword.SADDLE);
|
||||
} else if (property.equals("Station")) {
|
||||
return sa.isKeyword(Keyword.STATION);
|
||||
} else if (property.equals("Cycling")) {
|
||||
}else if (property.equals("Cycling")) {
|
||||
return sa.isCycling();
|
||||
} else if (property.equals("Dash")) {
|
||||
return sa.isDash();
|
||||
@@ -235,8 +237,6 @@ public class ForgeScript {
|
||||
return sa.isBoast();
|
||||
} else if (property.equals("Exhaust")) {
|
||||
return sa.isExhaust();
|
||||
} else if (property.equals("Mayhem")) {
|
||||
return sa.isMayhem();
|
||||
} else if (property.equals("Mutate")) {
|
||||
return sa.isMutate();
|
||||
} else if (property.equals("Ninjutsu")) {
|
||||
@@ -410,8 +410,6 @@ public class ForgeScript {
|
||||
return !sa.isPwAbility() && !sa.getRestrictions().isSorcerySpeed();
|
||||
}
|
||||
return true;
|
||||
} else if(property.startsWith("NamedAbility")) {
|
||||
return sa.getName().equals(property.substring(12));
|
||||
} else if (sa.getHostCard() != null) {
|
||||
return sa.getHostCard().hasProperty(property, sourceController, source, spellAbility);
|
||||
}
|
||||
|
||||
@@ -414,6 +414,19 @@ public class Game {
|
||||
return players;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the nonactive players who are still fighting to win, in turn order.
|
||||
*/
|
||||
public final PlayerCollection getNonactivePlayers() {
|
||||
// Don't use getPlayersInTurnOrder to prevent copying the player collection twice
|
||||
final PlayerCollection players = new PlayerCollection(ingamePlayers);
|
||||
players.remove(phaseHandler.getPlayerTurn());
|
||||
if (!getTurnOrder().isDefaultDirection()) {
|
||||
Collections.reverse(players);
|
||||
}
|
||||
return players;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the players who participated in match (regardless of outcome).
|
||||
* <i>Use this in UI and after match calculations</i>
|
||||
@@ -731,7 +744,7 @@ public class Game {
|
||||
if (!visitor.visitAll(player.getZone(ZoneType.Exile).getCards())) {
|
||||
return;
|
||||
}
|
||||
if (!visitor.visitAll(player.getCardsIn(ZoneType.PART_OF_COMMAND_ZONE))) {
|
||||
if (!visitor.visitAll(player.getZone(ZoneType.Command).getCards())) {
|
||||
return;
|
||||
}
|
||||
if (withSideboard && !visitor.visitAll(player.getZone(ZoneType.Sideboard).getCards())) {
|
||||
@@ -845,8 +858,6 @@ public class Game {
|
||||
p.revealFaceDownCards();
|
||||
}
|
||||
|
||||
// TODO free any mindslaves
|
||||
|
||||
for (Card c : cards) {
|
||||
// CR 800.4d if card is controlled by opponent, LTB should trigger
|
||||
if (c.getOwner().equals(p) && c.getController().equals(p)) {
|
||||
@@ -882,6 +893,8 @@ public class Game {
|
||||
}
|
||||
triggerList.put(c.getZone().getZoneType(), null, c);
|
||||
getAction().ceaseToExist(c, false);
|
||||
// CR 603.2f owner of trigger source lost game
|
||||
getTriggerHandler().clearDelayedTrigger(c);
|
||||
}
|
||||
} else {
|
||||
// return stolen permanents
|
||||
|
||||
@@ -57,8 +57,6 @@ import forge.item.PaperCard;
|
||||
import forge.util.*;
|
||||
import forge.util.collect.FCollection;
|
||||
import forge.util.collect.FCollectionView;
|
||||
import io.sentry.Breadcrumb;
|
||||
import io.sentry.Sentry;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.jgrapht.alg.cycle.SzwarcfiterLauerSimpleCycles;
|
||||
import org.jgrapht.graph.DefaultDirectedGraph;
|
||||
@@ -222,6 +220,10 @@ public class GameAction {
|
||||
//copied.setGamePieceType(GamePieceType.COPIED_SPELL);
|
||||
}
|
||||
|
||||
if (c.isTransformed()) {
|
||||
copied.incrementTransformedTimestamp();
|
||||
}
|
||||
|
||||
if (cause != null && cause.isSpell() && c.equals(cause.getHostCard())) {
|
||||
copied.setCastSA(cause);
|
||||
copied.setSplitStateToPlayAbility(cause);
|
||||
@@ -751,29 +753,26 @@ public class GameAction {
|
||||
|
||||
public final Card moveTo(final ZoneType name, final Card c, final int libPosition, SpellAbility cause, Map<AbilityKey, Object> params) {
|
||||
// Call specific functions to set PlayerZone, then move onto moveTo
|
||||
try {
|
||||
return switch (name) {
|
||||
case Hand -> moveToHand(c, cause, params);
|
||||
case Library -> moveToLibrary(c, libPosition, cause, params);
|
||||
case Battlefield -> moveToPlay(c, c.getController(), cause, params);
|
||||
case Graveyard -> moveToGraveyard(c, cause, params);
|
||||
case Exile -> !c.canExiledBy(cause, true) ? null : exile(c, cause, params);
|
||||
case Stack -> moveToStack(c, cause, params);
|
||||
case PlanarDeck, SchemeDeck, AttractionDeck, ContraptionDeck -> moveToVariantDeck(c, name, libPosition, cause, params);
|
||||
case Junkyard -> moveToJunkyard(c, cause, params);
|
||||
default -> moveTo(c.getOwner().getZone(name), c, cause); // sideboard will also get there
|
||||
};
|
||||
} catch (Exception e) {
|
||||
String msg = "GameAction:moveTo: Exception occured";
|
||||
|
||||
Breadcrumb bread = new Breadcrumb(msg);
|
||||
bread.setData("Card", c.getName());
|
||||
bread.setData("SA", cause.toString());
|
||||
bread.setData("ZoneType", name.name());
|
||||
bread.setData("Player", c.getOwner());
|
||||
Sentry.addBreadcrumb(bread);
|
||||
|
||||
throw new RuntimeException("Error in GameAction moveTo " + c.getName() + " to Player Zone " + name.name(), e);
|
||||
switch(name) {
|
||||
case Hand: return moveToHand(c, cause, params);
|
||||
case Library: return moveToLibrary(c, libPosition, cause, params);
|
||||
case Battlefield: return moveToPlay(c, c.getController(), cause, params);
|
||||
case Graveyard: return moveToGraveyard(c, cause, params);
|
||||
case Exile:
|
||||
if (!c.canExiledBy(cause, true)) {
|
||||
return null;
|
||||
}
|
||||
return exile(c, cause, params);
|
||||
case Stack: return moveToStack(c, cause, params);
|
||||
case PlanarDeck:
|
||||
case SchemeDeck:
|
||||
case AttractionDeck:
|
||||
case ContraptionDeck:
|
||||
return moveToVariantDeck(c, name, libPosition, cause, params);
|
||||
case Junkyard:
|
||||
return moveToJunkyard(c, cause, params);
|
||||
default: // sideboard will also get there
|
||||
return moveTo(c.getOwner().getZone(name), c, cause);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -975,7 +974,6 @@ public class GameAction {
|
||||
// in some corner cases there's no zone yet (copied spell that failed targeting)
|
||||
if (z != null) {
|
||||
z.remove(c);
|
||||
c.setZone(c.getOwner().getZone(ZoneType.None));
|
||||
if (z.is(ZoneType.Battlefield)) {
|
||||
c.runLeavesPlayCommands();
|
||||
}
|
||||
@@ -1602,7 +1600,9 @@ public class GameAction {
|
||||
}
|
||||
|
||||
// recheck the game over condition at this point to make sure no other win conditions apply now.
|
||||
checkGameOverCondition();
|
||||
if (!game.isGameOver()) {
|
||||
checkGameOverCondition();
|
||||
}
|
||||
|
||||
if (game.getAge() != GameStage.Play) {
|
||||
return false;
|
||||
@@ -1822,8 +1822,8 @@ public class GameAction {
|
||||
|
||||
private boolean stateBasedAction704_5q(Card c) {
|
||||
boolean checkAgain = false;
|
||||
final CounterType p1p1 = CounterEnumType.P1P1;
|
||||
final CounterType m1m1 = CounterEnumType.M1M1;
|
||||
final CounterType p1p1 = CounterType.get(CounterEnumType.P1P1);
|
||||
final CounterType m1m1 = CounterType.get(CounterEnumType.M1M1);
|
||||
int plusOneCounters = c.getCounters(p1p1);
|
||||
int minusOneCounters = c.getCounters(m1m1);
|
||||
if (plusOneCounters > 0 && minusOneCounters > 0) {
|
||||
@@ -1843,7 +1843,7 @@ public class GameAction {
|
||||
return checkAgain;
|
||||
}
|
||||
private boolean stateBasedAction704_5r(Card c) {
|
||||
final CounterType dreamType = CounterEnumType.DREAM;
|
||||
final CounterType dreamType = CounterType.get(CounterEnumType.DREAM);
|
||||
|
||||
int old = c.getCounters(dreamType);
|
||||
if (old <= 0) {
|
||||
@@ -1883,10 +1883,6 @@ public class GameAction {
|
||||
}
|
||||
|
||||
public void checkGameOverCondition() {
|
||||
if (game.isGameOver()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// award loses as SBE
|
||||
GameEndReason reason = null;
|
||||
List<Player> losers = null;
|
||||
@@ -2226,13 +2222,6 @@ public class GameAction {
|
||||
}
|
||||
}
|
||||
|
||||
public void revealUnsupported(Map<Player, List<PaperCard>> unsupported) {
|
||||
// Notify players
|
||||
for (Player p : game.getPlayers()) {
|
||||
p.getController().revealUnsupported(unsupported);
|
||||
}
|
||||
}
|
||||
|
||||
/** Delivers a message to all players. (use reveal to show Cards) */
|
||||
public void notifyOfValue(SpellAbility saSource, GameObject relatedTarget, String value, Player playerExcept) {
|
||||
if (saSource != null) {
|
||||
|
||||
@@ -125,22 +125,10 @@ public final class GameActionUtil {
|
||||
|
||||
// need to be done there before static abilities does reset the card
|
||||
// These Keywords depend on the Mana Cost of for Split Cards
|
||||
if (sa.isBasicSpell()) {
|
||||
if (sa.isBasicSpell() && !sa.isLandAbility()) {
|
||||
for (final KeywordInterface inst : source.getKeywords()) {
|
||||
final String keyword = inst.getOriginal();
|
||||
|
||||
if (keyword.startsWith("Mayhem")) {
|
||||
if (!source.isInZone(ZoneType.Graveyard) || !source.wasDiscarded() || !source.enteredThisTurn()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
alternatives.add(getGraveyardSpellByKeyword(inst, sa, activator, AlternativeCost.Mayhem));
|
||||
}
|
||||
|
||||
if (sa.isLandAbility()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (keyword.startsWith("Escape")) {
|
||||
if (!source.isInZone(ZoneType.Graveyard)) {
|
||||
continue;
|
||||
@@ -177,7 +165,26 @@ public final class GameActionUtil {
|
||||
continue;
|
||||
}
|
||||
|
||||
alternatives.add(getGraveyardSpellByKeyword(inst, sa, activator, AlternativeCost.Flashback));
|
||||
SpellAbility flashback = null;
|
||||
|
||||
// there is a flashback cost (and not the cards cost)
|
||||
if (keyword.contains(":")) { // K:Flashback:Cost:ExtraParams:ExtraDescription
|
||||
final String[] k = keyword.split(":");
|
||||
flashback = sa.copyWithManaCostReplaced(activator, new Cost(k[1], false));
|
||||
String extraParams = k.length > 2 ? k[2] : "";
|
||||
if (!extraParams.isEmpty()) {
|
||||
for (Map.Entry<String, String> param : AbilityFactory.getMapParams(extraParams).entrySet()) {
|
||||
flashback.putParam(param.getKey(), param.getValue());
|
||||
}
|
||||
}
|
||||
} else { // same cost as original (e.g. Otaria plane)
|
||||
flashback = sa.copy(activator);
|
||||
}
|
||||
flashback.setAlternativeCost(AlternativeCost.Flashback);
|
||||
flashback.getRestrictions().setZone(ZoneType.Graveyard);
|
||||
flashback.setKeyword(inst);
|
||||
flashback.setIntrinsic(inst.isIntrinsic());
|
||||
alternatives.add(flashback);
|
||||
} else if (keyword.startsWith("Harmonize")) {
|
||||
if (!source.isInZone(ZoneType.Graveyard)) {
|
||||
continue;
|
||||
@@ -187,7 +194,25 @@ public final class GameActionUtil {
|
||||
continue;
|
||||
}
|
||||
|
||||
alternatives.add(getGraveyardSpellByKeyword(inst, sa, activator, AlternativeCost.Harmonize));
|
||||
SpellAbility harmonize = null;
|
||||
|
||||
if (keyword.contains(":")) {
|
||||
final String[] k = keyword.split(":");
|
||||
harmonize = sa.copyWithManaCostReplaced(activator, new Cost(k[1], false));
|
||||
String extraParams = k.length > 2 ? k[2] : "";
|
||||
if (!extraParams.isEmpty()) {
|
||||
for (Map.Entry<String, String> param : AbilityFactory.getMapParams(extraParams).entrySet()) {
|
||||
harmonize.putParam(param.getKey(), param.getValue());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
harmonize = sa.copy(activator);
|
||||
}
|
||||
harmonize.setAlternativeCost(AlternativeCost.Harmonize);
|
||||
harmonize.getRestrictions().setZone(ZoneType.Graveyard);
|
||||
harmonize.setKeyword(inst);
|
||||
harmonize.setIntrinsic(inst.isIntrinsic());
|
||||
alternatives.add(harmonize);
|
||||
} else if (keyword.startsWith("Foretell")) {
|
||||
// Foretell cast only from Exile
|
||||
if (!source.isInZone(ZoneType.Exile) || !source.isForetold() || source.enteredThisTurn() ||
|
||||
@@ -242,7 +267,6 @@ public final class GameActionUtil {
|
||||
}
|
||||
stackCopy.setLastKnownZone(game.getStackZone());
|
||||
stackCopy.setCastFrom(oldZone);
|
||||
stackCopy.setCastSA(sa);
|
||||
lkicheck = true;
|
||||
|
||||
stackCopy.clearStaticChangedCardKeywords(false);
|
||||
@@ -293,30 +317,6 @@ public final class GameActionUtil {
|
||||
return alternatives;
|
||||
}
|
||||
|
||||
public static SpellAbility getGraveyardSpellByKeyword(KeywordInterface inst, SpellAbility sa, Player activator, AlternativeCost altCost) {
|
||||
String keyword = inst.getOriginal();
|
||||
SpellAbility newSA = null;
|
||||
|
||||
// there is a flashback cost (and not the cards cost)
|
||||
if (keyword.contains(":")) { // K:Flashback:Cost:ExtraParams:ExtraDescription
|
||||
final String[] k = keyword.split(":");
|
||||
newSA = sa.copyWithManaCostReplaced(activator, new Cost(k[1], false));
|
||||
String extraParams = k.length > 2 ? k[2] : "";
|
||||
if (!extraParams.isEmpty()) {
|
||||
for (Map.Entry<String, String> param : AbilityFactory.getMapParams(extraParams).entrySet()) {
|
||||
newSA.putParam(param.getKey(), param.getValue());
|
||||
}
|
||||
}
|
||||
} else { // same cost as original (e.g. Otaria plane)
|
||||
newSA = sa.copy(activator);
|
||||
}
|
||||
newSA.setAlternativeCost(altCost);
|
||||
newSA.getRestrictions().setZone(ZoneType.Graveyard);
|
||||
newSA.setKeyword(inst);
|
||||
newSA.setIntrinsic(inst.isIntrinsic());
|
||||
return newSA;
|
||||
}
|
||||
|
||||
public static List<SpellAbility> getMayPlaySpellOptions(final SpellAbility sa, final Card source, final Player activator, boolean altCostOnly) {
|
||||
final List<SpellAbility> alternatives = Lists.newArrayList();
|
||||
|
||||
@@ -993,6 +993,9 @@ public final class GameActionUtil {
|
||||
oldCard.setBackSide(false);
|
||||
oldCard.setState(oldCard.getFaceupCardStateName(), true);
|
||||
oldCard.unanimateBestow();
|
||||
if (ability.isDisturb() || ability.hasParam("CastTransformed")) {
|
||||
oldCard.undoIncrementTransformedTimestamp();
|
||||
}
|
||||
|
||||
if (ability.hasParam("Prototype")) {
|
||||
oldCard.removeCloneState(oldCard.getPrototypeTimestamp());
|
||||
|
||||
@@ -33,18 +33,17 @@ import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CounterEnumType;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.event.GameEventCardAttachment;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.keyword.KeywordInterface;
|
||||
import forge.game.keyword.KeywordWithType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.replacement.ReplacementEffect;
|
||||
import forge.game.replacement.ReplacementType;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityCantAttach;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.Lang;
|
||||
|
||||
public abstract class GameEntity extends GameObject implements IIdentifiable {
|
||||
protected int id;
|
||||
@@ -198,12 +197,14 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
|
||||
public final void addAttachedCard(final Card c) {
|
||||
if (attachedCards.add(c)) {
|
||||
updateAttachedCards();
|
||||
getGame().fireEvent(new GameEventCardAttachment(c, null, this));
|
||||
}
|
||||
}
|
||||
|
||||
public final void removeAttachedCard(final Card c) {
|
||||
if (attachedCards.remove(c)) {
|
||||
updateAttachedCards();
|
||||
getGame().fireEvent(new GameEventCardAttachment(c, this, null));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,83 +222,63 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
|
||||
return canBeAttached(attach, sa, false);
|
||||
}
|
||||
public boolean canBeAttached(final Card attach, SpellAbility sa, boolean checkSBA) {
|
||||
return cantBeAttachedMsg(attach, sa, checkSBA) == null;
|
||||
}
|
||||
|
||||
public String cantBeAttachedMsg(final Card attach, SpellAbility sa) {
|
||||
return cantBeAttachedMsg(attach, sa, false);
|
||||
}
|
||||
public String cantBeAttachedMsg(final Card attach, SpellAbility sa, boolean checkSBA) {
|
||||
if (!attach.isAttachment()) {
|
||||
return attach.getName() + " is not an attachment";
|
||||
}
|
||||
if (equals(attach)) {
|
||||
return attach.getName() + " can't attach to itself";
|
||||
}
|
||||
|
||||
if (attach.isCreature() && !attach.hasKeyword(Keyword.RECONFIGURE)) {
|
||||
return attach.getName() + " is a creature without reconfigure";
|
||||
// master mode
|
||||
if (!attach.isAttachment() || (attach.isCreature() && !attach.hasKeyword(Keyword.RECONFIGURE))
|
||||
|| equals(attach)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (attach.isPhasedOut()) {
|
||||
return attach.getName() + " is phased out";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (attach.isAura()) {
|
||||
String msg = cantBeEnchantedByMsg(attach);
|
||||
if (msg != null) {
|
||||
return msg;
|
||||
}
|
||||
// check for rules
|
||||
if (attach.isAura() && !canBeEnchantedBy(attach)) {
|
||||
return false;
|
||||
}
|
||||
if (attach.isEquipment()) {
|
||||
String msg = cantBeEquippedByMsg(attach, sa);
|
||||
if (msg != null) {
|
||||
return msg;
|
||||
}
|
||||
if (attach.isEquipment() && !canBeEquippedBy(attach, sa)) {
|
||||
return false;
|
||||
}
|
||||
if (attach.isFortification()) {
|
||||
String msg = cantBeFortifiedByMsg(attach);
|
||||
if (msg != null) {
|
||||
return msg;
|
||||
}
|
||||
if (attach.isFortification() && !canBeFortifiedBy(attach)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
StaticAbility stAb = StaticAbilityCantAttach.cantAttach(this, attach, checkSBA);
|
||||
if (stAb != null) {
|
||||
return stAb.toString();
|
||||
// check for can't attach static
|
||||
if (StaticAbilityCantAttach.cantAttach(this, attach, checkSBA)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
// true for all
|
||||
return true;
|
||||
}
|
||||
|
||||
protected String cantBeEquippedByMsg(final Card aura, SpellAbility sa) {
|
||||
protected boolean canBeEquippedBy(final Card aura, SpellAbility sa) {
|
||||
/**
|
||||
* Equip only to Creatures which are cards
|
||||
*/
|
||||
return false;
|
||||
}
|
||||
|
||||
protected boolean canBeFortifiedBy(final Card aura) {
|
||||
/**
|
||||
* Equip only to Lands which are cards
|
||||
*/
|
||||
return getName() + " is not a Creature";
|
||||
return false;
|
||||
}
|
||||
|
||||
protected String cantBeFortifiedByMsg(final Card fort) {
|
||||
/**
|
||||
* Equip only to Lands which are cards
|
||||
*/
|
||||
return getName() + " is not a Land";
|
||||
}
|
||||
|
||||
protected String cantBeEnchantedByMsg(final Card aura) {
|
||||
protected boolean canBeEnchantedBy(final Card aura) {
|
||||
if (!aura.hasKeyword(Keyword.ENCHANT)) {
|
||||
return "No Enchant Keyword";
|
||||
return false;
|
||||
}
|
||||
for (KeywordInterface ki : aura.getKeywords(Keyword.ENCHANT)) {
|
||||
if (ki instanceof KeywordWithType kwt) {
|
||||
String v = kwt.getValidType();
|
||||
String desc = kwt.getTypeDescription();
|
||||
if (!isValid(v.split(","), aura.getController(), aura, null)) {
|
||||
return getName() + " is not " + Lang.nounWithAmount(1, desc);
|
||||
}
|
||||
String k = ki.getOriginal();
|
||||
String m[] = k.split(":");
|
||||
String v = m[1];
|
||||
if (!isValid(v.split(","), aura.getController(), aura, null)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean hasCounters() {
|
||||
@@ -324,6 +305,9 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
|
||||
Integer value = counters.get(counterName);
|
||||
return value == null ? 0 : value;
|
||||
}
|
||||
public final int getCounters(final CounterEnumType counterType) {
|
||||
return getCounters(CounterType.get(counterType));
|
||||
}
|
||||
|
||||
public void setCounters(final CounterType counterType, final Integer num) {
|
||||
if (num <= 0) {
|
||||
@@ -332,6 +316,9 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
|
||||
counters.put(counterType, num);
|
||||
}
|
||||
}
|
||||
public void setCounters(final CounterEnumType counterType, final Integer num) {
|
||||
setCounters(CounterType.get(counterType), num);
|
||||
}
|
||||
|
||||
abstract public void setCounters(final Map<CounterType, Integer> allCounters);
|
||||
|
||||
@@ -341,6 +328,10 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
|
||||
abstract public int subtractCounter(final CounterType counterName, final int n, final Player remover);
|
||||
abstract public void clearCounters();
|
||||
|
||||
public boolean canReceiveCounters(final CounterEnumType type) {
|
||||
return canReceiveCounters(CounterType.get(type));
|
||||
}
|
||||
|
||||
public final void addCounter(final CounterType counterType, int n, final Player source, GameEntityCounterTable table) {
|
||||
if (n <= 0 || !canReceiveCounters(counterType)) {
|
||||
// As per rule 107.1b
|
||||
@@ -360,7 +351,18 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
|
||||
table.put(source, this, counterType, n);
|
||||
}
|
||||
|
||||
public final void addCounter(final CounterEnumType counterType, final int n, final Player source, GameEntityCounterTable table) {
|
||||
addCounter(CounterType.get(counterType), n, source, table);
|
||||
}
|
||||
|
||||
public int subtractCounter(final CounterEnumType counterName, final int n, final Player remover) {
|
||||
return subtractCounter(CounterType.get(counterName), n, remover);
|
||||
}
|
||||
|
||||
abstract public void addCounterInternal(final CounterType counterType, final int n, final Player source, final boolean fireEvents, GameEntityCounterTable table, Map<AbilityKey, Object> params);
|
||||
public void addCounterInternal(final CounterEnumType counterType, final int n, final Player source, final boolean fireEvents, GameEntityCounterTable table, Map<AbilityKey, Object> params) {
|
||||
addCounterInternal(CounterType.get(counterType), n, source, fireEvents, table, params);
|
||||
}
|
||||
public Integer getCounterMax(final CounterType counterType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ public enum GameLogEntryType {
|
||||
TURN("Turn"),
|
||||
MULLIGAN("Mulligan"),
|
||||
ANTE("Ante"),
|
||||
DRAFT("Draft"),
|
||||
ZONE_CHANGE("Zone Change"),
|
||||
PLAYER_CONTROL("Player control"),
|
||||
COMBAT("Combat"),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user