Merge pull request 'master' (#19) from External/mage:master into master
All checks were successful
/ example-docker-compose (push) Successful in 15m4s

Reviewed-on: #19
This commit is contained in:
Failure 2025-03-02 17:39:59 -08:00
commit d1ca46fd85
111 changed files with 3335 additions and 976 deletions

View file

@ -7,7 +7,10 @@ import mage.abilities.dynamicvalue.common.ControllerSpeedCount;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.effects.Effect;
import mage.cards.Card;
import mage.constants.*;
import mage.constants.Duration;
import mage.constants.Layer;
import mage.constants.Outcome;
import mage.constants.SubLayer;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.util.CardUtil;
@ -22,7 +25,7 @@ public class MaxSpeedAbility extends StaticAbility {
}
public MaxSpeedAbility(Ability ability) {
super(Zone.ALL, new MaxSpeedAbilityEffect(ability));
super(ability.getZone(), new MaxSpeedAbilityEffect(ability));
}
private MaxSpeedAbility(final MaxSpeedAbility ability) {

View file

@ -0,0 +1,76 @@
package mage.abilities.dynamicvalue.common;
import mage.abilities.Ability;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.effects.Effect;
import mage.abilities.hint.Hint;
import mage.abilities.hint.ValueHint;
import mage.game.Game;
import mage.watchers.common.SpellsCastWatcher;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.UUID;
public enum InstantAndSorceryCastThisTurn implements DynamicValue
{
YOU("you've cast"),
ALL("all players have cast"),
OPPONENTS("your opponents have cast");
private final String message;
private final ValueHint hint;
InstantAndSorceryCastThisTurn(String message) {
this.message = "Instant and sorcery spells " + message + " this turn";
this.hint = new ValueHint(this.message, this);
}
@Override
public int calculate(Game game, Ability sourceAbility, Effect effect) {
return getSpellsCastThisTurn(game, sourceAbility);
}
@Override
public InstantAndSorceryCastThisTurn copy() {
return this;
}
@Override
public String getMessage() {
return this.message;
}
public Hint getHint() {
return this.hint;
}
private int getSpellsCastThisTurn(Game game, Ability ability) {
Collection<UUID> playerIds;
switch (this) {
case YOU:
playerIds = Collections.singletonList(ability.getControllerId());
break;
case ALL:
playerIds = game.getState().getPlayersInRange(ability.getControllerId(), game);
break;
case OPPONENTS:
playerIds = game.getOpponents(ability.getControllerId());
break;
default:
return 0;
}
SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class);
if (watcher == null) {
return 0;
}
return (int) playerIds.stream()
.map(watcher::getSpellsCastThisTurn)
.flatMap(Collection::stream)
.filter(Objects::nonNull)
.filter(spell -> spell.isInstantOrSorcery(game))
.count();
}
}

View file

@ -22,7 +22,6 @@ import mage.game.stack.Spell;
import mage.players.ManaPoolItem;
import mage.players.Player;
import mage.target.common.TargetCardInHand;
import mage.util.trace.TraceInfo;
import org.apache.log4j.Logger;
import java.io.Serializable;
@ -1405,88 +1404,6 @@ public class ContinuousEffects implements Serializable {
return controllerFound;
}
/**
* Debug only: prints out a status of the currently existing continuous effects
*
* @param game
*/
public void traceContinuousEffects(Game game) {
game.getContinuousEffects().getLayeredEffects(game);
logger.info("-------------------------------------------------------------------------------------------------");
int numberEffects = 0;
for (ContinuousEffectsList list : allEffectsLists) {
numberEffects += list.size();
}
logger.info("Turn: " + game.getTurnNum() + " - currently existing continuous effects: " + numberEffects);
logger.info("layeredEffects ...................: " + layeredEffects.size());
logger.info("continuousRuleModifyingEffects ...: " + continuousRuleModifyingEffects.size());
logger.info("replacementEffects ...............: " + replacementEffects.size());
logger.info("preventionEffects ................: " + preventionEffects.size());
logger.info("requirementEffects ...............: " + requirementEffects.size());
logger.info("restrictionEffects ...............: " + restrictionEffects.size());
logger.info("restrictionUntapNotMoreThanEffects: " + restrictionUntapNotMoreThanEffects.size());
logger.info("costModificationEffects ..........: " + costModificationEffects.size());
logger.info("spliceCardEffects ................: " + spliceCardEffects.size());
logger.info("asThoughEffects:");
for (Map.Entry<AsThoughEffectType, ContinuousEffectsList<AsThoughEffect>> entry : asThoughEffectsMap.entrySet()) {
logger.info("... " + entry.getKey().toString() + ": " + entry.getValue().size());
}
logger.info("applyStatus ....................: " + (applyStatus != null ? "exists" : "null"));
logger.info("auraReplacementEffect ............: " + (continuousRuleModifyingEffects != null ? "exists" : "null"));
Map<String, TraceInfo> orderedEffects = new TreeMap<>();
traceAddContinuousEffects(orderedEffects, layeredEffects, game, "layeredEffects................");
traceAddContinuousEffects(orderedEffects, continuousRuleModifyingEffects, game, "continuousRuleModifyingEffects");
traceAddContinuousEffects(orderedEffects, replacementEffects, game, "replacementEffects............");
traceAddContinuousEffects(orderedEffects, preventionEffects, game, "preventionEffects.............");
traceAddContinuousEffects(orderedEffects, requirementEffects, game, "requirementEffects............");
traceAddContinuousEffects(orderedEffects, restrictionEffects, game, "restrictionEffects............");
traceAddContinuousEffects(orderedEffects, restrictionUntapNotMoreThanEffects, game, "restrictionUntapNotMore...");
traceAddContinuousEffects(orderedEffects, costModificationEffects, game, "costModificationEffects.......");
traceAddContinuousEffects(orderedEffects, spliceCardEffects, game, "spliceCardEffects.............");
for (Map.Entry<AsThoughEffectType, ContinuousEffectsList<AsThoughEffect>> entry : asThoughEffectsMap.entrySet()) {
traceAddContinuousEffects(orderedEffects, entry.getValue(), game, entry.getKey().toString());
}
String playerName = "";
for (Map.Entry<String, TraceInfo> entry : orderedEffects.entrySet()) {
if (!entry.getValue().getPlayerName().equals(playerName)) {
playerName = entry.getValue().getPlayerName();
logger.info("--- Player: " + playerName + " --------------------------------");
}
logger.info(entry.getValue().getInfo()
+ " " + entry.getValue().getSourceName()
+ " " + entry.getValue().getDuration().name()
+ " " + entry.getValue().getRule()
+ " (Order: " + entry.getValue().getOrder() + ")"
);
}
logger.info("---- End trace Continuous effects --------------------------------------------------------------------------");
}
public static void traceAddContinuousEffects(Map orderedEffects, ContinuousEffectsList<?> cel, Game game, String listName) {
for (ContinuousEffect effect : cel) {
Set<Ability> abilities = cel.getAbility(effect.getId());
for (Ability ability : abilities) {
Player controller = game.getPlayer(ability.getControllerId());
MageObject source = game.getObject(ability.getSourceId());
TraceInfo traceInfo = new TraceInfo();
traceInfo.setInfo(listName);
traceInfo.setOrder(effect.getOrder());
if (ability instanceof MageSingleton) {
traceInfo.setPlayerName("Mage Singleton");
traceInfo.setSourceName("Mage Singleton");
} else {
traceInfo.setPlayerName(controller == null ? "no controller" : controller.getName());
traceInfo.setSourceName(source == null ? "no source" : source.getIdName());
}
traceInfo.setRule(ability.getRule());
traceInfo.setAbilityId(ability.getId());
traceInfo.setEffectId(effect.getId());
traceInfo.setDuration(effect.getDuration());
orderedEffects.put(traceInfo.getPlayerName() + traceInfo.getSourceName() + effect.getId() + ability.getId(), traceInfo);
}
}
}
public int getTotalEffectsCount() {
return allEffectsLists.stream().mapToInt(ContinuousEffectsList::size).sum();
}

View file

@ -7,6 +7,7 @@ import mage.abilities.effects.Effect;
import mage.abilities.effects.OneShotEffect;
import mage.game.Game;
import mage.target.targetpointer.TargetPointer;
import mage.util.CardUtil;
/**
* @author BetaSteward_at_googlemail.com
@ -67,7 +68,7 @@ public class CreateDelayedTriggeredAbilityEffect extends OneShotEffect {
return staticText;
}
if (ability.getRuleVisible()) {
return rulePrefix + ability.getRule();
return rulePrefix + CardUtil.getTextWithFirstCharLowerCase(ability.getRule());
} else {
return "";
}

View file

@ -10,14 +10,26 @@ import mage.target.targetpointer.FixedTarget;
public class CrewsVehicleSourceTriggeredAbility extends TriggeredAbilityImpl {
private final boolean mountsAlso;
private final boolean yourMainPhaseOnly;
public CrewsVehicleSourceTriggeredAbility(Effect effect) {
this(effect, false, false);
}
public CrewsVehicleSourceTriggeredAbility(Effect effect, boolean mountsAlso, boolean yourMainPhaseOnly) {
super(Zone.BATTLEFIELD, effect, false);
this.addIcon(CardIconImpl.ABILITY_CREW);
setTriggerPhrase("Whenever {this} crews a Vehicle, ");
this.mountsAlso = mountsAlso;
this.yourMainPhaseOnly = yourMainPhaseOnly;
setTriggerPhrase("Whenever {this}" + (mountsAlso ? " saddles a Mount or" : "") +
" crews a Vehicle" + (yourMainPhaseOnly ? " during your main phase" : "") + ", ");
}
protected CrewsVehicleSourceTriggeredAbility(final CrewsVehicleSourceTriggeredAbility ability) {
super(ability);
this.mountsAlso = ability.mountsAlso;
this.yourMainPhaseOnly = ability.yourMainPhaseOnly;
}
@Override
@ -27,11 +39,14 @@ public class CrewsVehicleSourceTriggeredAbility extends TriggeredAbilityImpl {
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.CREWED_VEHICLE;
return event.getType() == GameEvent.EventType.CREWED_VEHICLE || (mountsAlso && event.getType() == GameEvent.EventType.SADDLED_MOUNT);
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (yourMainPhaseOnly && !(game.isMainPhase() && this.isControlledBy(game.getActivePlayerId()))) {
return false;
}
if (event.getTargetId().equals(getSourceId())) {
for (Effect effect : getEffects()) {
// set the vehicle id as target

View file

@ -45,22 +45,22 @@ public class GainAbilityControlledSpellsEffect extends ContinuousEffectImpl {
}
for (Card card : game.getExile().getAllCardsByRange(game, source.getControllerId())) {
if (filter.match(card, game)) {
if (filter.match(card, player.getId(), source, game)) {
game.getState().addOtherAbility(card, ability);
}
}
for (Card card : player.getLibrary().getCards(game)) {
if (filter.match(card, game)) {
if (filter.match(card, player.getId(), source, game)) {
game.getState().addOtherAbility(card, ability);
}
}
for (Card card : player.getHand().getCards(game)) {
if (filter.match(card, game)) {
if (filter.match(card, player.getId(), source, game)) {
game.getState().addOtherAbility(card, ability);
}
}
for (Card card : player.getGraveyard().getCards(game)) {
if (filter.match(card, game)) {
if (filter.match(card, player.getId(), source, game)) {
game.getState().addOtherAbility(card, ability);
}
}
@ -68,7 +68,7 @@ public class GainAbilityControlledSpellsEffect extends ContinuousEffectImpl {
// workaround to gain cost reduction abilities to commanders before cast (make it playable)
game.getCommanderCardsFromCommandZone(player, CommanderCardType.ANY)
.stream()
.filter(card -> filter.match(card, game))
.filter(card -> filter.match(card, player.getId(), source, game))
.forEach(card -> game.getState().addOtherAbility(card, ability));
for (StackObject stackObject : game.getStack()) {
@ -77,7 +77,7 @@ public class GainAbilityControlledSpellsEffect extends ContinuousEffectImpl {
}
// TODO: Distinguish "you cast" to exclude copies
Card card = game.getCard(stackObject.getSourceId());
if (card != null && filter.match((Spell) stackObject, game)) {
if (card != null && filter.match((Spell) stackObject, player.getId(), source, game)) {
game.getState().addOtherAbility(card, ability);
}
}

View file

@ -12,27 +12,37 @@ import mage.filter.FilterCard;
import mage.game.Game;
import mage.players.Player;
import mage.target.common.TargetCardInLibrary;
import mage.util.CardUtil;
/**
* @author BetaSteward_at_googlemail.com, edited by Cguy7777
*/
public class SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect extends SearchEffect {
public class SearchLibraryPutOntoBattlefieldTappedRestInHandEffect extends SearchEffect {
private static final FilterCard filter = new FilterCard("card to put on the battlefield tapped");
private final FilterCard filter;
private final int numToBattlefield;
public SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect(TargetCardInLibrary target) {
public SearchLibraryPutOntoBattlefieldTappedRestInHandEffect(TargetCardInLibrary target, int numToBattlefield) {
super(target, Outcome.PutLandInPlay);
staticText = "search your library for " + target.getDescription() +
", reveal those cards, put one onto the battlefield tapped and the other into your hand, then shuffle";
", reveal those cards, put " + CardUtil.numberToText(numToBattlefield) + " onto the battlefield tapped and the other into your hand, then shuffle";
this.filter = new FilterCard((numToBattlefield > 1 ? "cards" : "card") + "to put on the battlefield tapped");
this.numToBattlefield = numToBattlefield;
}
protected SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect(final SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect effect) {
public SearchLibraryPutOntoBattlefieldTappedRestInHandEffect(TargetCardInLibrary target) {
this(target, 1);
}
protected SearchLibraryPutOntoBattlefieldTappedRestInHandEffect(final SearchLibraryPutOntoBattlefieldTappedRestInHandEffect effect) {
super(effect);
this.filter = effect.filter.copy();
this.numToBattlefield = effect.numToBattlefield;
}
@Override
public SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect copy() {
return new SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect(this);
public SearchLibraryPutOntoBattlefieldTappedRestInHandEffect copy() {
return new SearchLibraryPutOntoBattlefieldTappedRestInHandEffect(this);
}
@Override
@ -49,14 +59,15 @@ public class SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect extends Se
controller.revealCards(sourceObject.getIdName(), revealed, game);
if (target.getTargets().size() >= 2) {
TargetCardInLibrary targetCardToBattlefield = new TargetCardInLibrary(filter);
controller.choose(Outcome.PutLandInPlay, revealed, targetCardToBattlefield, source, game);
int maxToBattlefield = Math.min(numToBattlefield, target.getTargets().size());
TargetCardInLibrary targetCardsToBattlefield = new TargetCardInLibrary(maxToBattlefield, filter);
controller.choose(Outcome.PutLandInPlay, revealed, targetCardsToBattlefield, source, game);
Card cardToBattlefield = revealed.get(targetCardToBattlefield.getFirstTarget(), game);
Cards cardsToBattlefield = new CardsImpl(targetCardsToBattlefield.getTargets());
Cards cardsToHand = new CardsImpl(revealed);
if (cardToBattlefield != null) {
controller.moveCards(cardToBattlefield, Zone.BATTLEFIELD, source, game, true, false, false, null);
cardsToHand.remove(cardToBattlefield);
if (!cardsToBattlefield.isEmpty()) {
controller.moveCards(cardsToBattlefield.getCards(game), Zone.BATTLEFIELD, source, game, true, false, false, null);
cardsToHand.removeAll(cardsToBattlefield);
}
controller.moveCardsToHandWithInfo(cardsToHand, source, game, true);

View file

@ -551,7 +551,7 @@ public enum SubType {
@Override
public String toString() {
return "Subtype(" + subtype + ')';
return "Subtype(" + subtype + ')'; // warning, do not change until refactor code like predicate.toString().equals
}
}

View file

@ -3814,7 +3814,7 @@ public abstract class GameImpl implements Game {
@Override
public boolean endTurn(Ability source) {
getTurn().endTurn(this, getActivePlayerId(), source);
getTurn().endTurn(this, source);
return true;
}

View file

@ -33,7 +33,6 @@ import mage.target.common.TargetControlledPermanent;
import mage.target.common.TargetDefender;
import mage.util.CardUtil;
import mage.util.Copyable;
import mage.util.trace.TraceUtil;
import org.apache.log4j.Logger;
import java.io.Serializable;
@ -744,8 +743,6 @@ public class Combat implements Serializable, Copyable<Combat> {
game.getCombat().logBlockerInfo(defender, game);
}
}
// tool to catch the bug about flyers blocked by non flyers or intimidate blocked by creatures with other colors
TraceUtil.traceCombatIfNeeded(game, game.getCombat());
}
private void makeSureItsNotComputer(Player controller) {
@ -761,7 +758,7 @@ public class Combat implements Serializable, Copyable<Combat> {
* Add info about attacker blocked by blocker to the game log
*/
private void logBlockerInfo(Player defender, Game game) {
boolean shownDefendingPlayer = game.getPlayers().size() < 3; // only two players no need to saw the attacked player
boolean shownDefendingPlayer = game.getPlayers().size() <= 2; // 1 vs 1 game, no need to saw the attacked player
for (CombatGroup group : game.getCombat().getGroups()) {
if (group.defendingPlayerId.equals(defender.getId())) {
if (!shownDefendingPlayer) {

View file

@ -70,7 +70,8 @@ class DackFaydenEmblemTriggeredAbility extends TriggeredAbilityImpl {
Spell spell = game.getStack().getSpell(event.getTargetId());
if (spell != null) {
SpellAbility spellAbility = spell.getSpellAbility();
for (Mode mode : spellAbility.getModes().values()) {
for (UUID modeId : spellAbility.getModes().getSelectedModes()) {
Mode mode = spellAbility.getModes().get(modeId);
for (Target target : mode.getTargets()) {
if (!target.isNotTarget()) {
for (UUID targetId : target.getTargets()) {

View file

@ -16,6 +16,7 @@ import mage.counters.CounterType;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.game.command.Emblem;
import mage.game.events.GameEvent;
import mage.players.Player;
/**
@ -106,8 +107,13 @@ class RadiationEffect extends OneShotEffect {
Cards milled = player.millCards(amount, source, game);
int countNonLand = milled.count(StaticFilters.FILTER_CARD_NON_LAND, player.getId(), source, game);
if (countNonLand > 0) {
// TODO: support gaining life instead with [[Strong, the Brutish Thespian]]
player.loseLife(countNonLand, game, source, false);
GameEvent event = new GameEvent(GameEvent.EventType.RADIATION_GAIN_LIFE, null, source, player.getId(), amount, false);
if (game.replaceEvent(event)) {
player.gainLife(countNonLand, game, source);
} else {
player.loseLife(countNonLand, game, source, false);
}
player.loseCounters(CounterType.RAD.getName(), countNonLand, source, game);
}
return true;

View file

@ -672,6 +672,9 @@ public class GameEvent implements Serializable {
playerId player who gave the gift
*/
GAVE_GIFT,
/* rad counter life loss/gain effect
*/
RADIATION_GAIN_LIFE,
// custom events - must store some unique data to track
CUSTOM_EVENT;

View file

@ -0,0 +1,36 @@
package mage.game.permanent.token;
import mage.MageInt;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.continuous.GainAbilityControlledEffect;
import mage.abilities.keyword.HorsemanshipAbility;
import mage.constants.Duration;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.filter.common.FilterCreaturePermanent;
/**
* @author padfoot
*/
public final class TheGirlInTheFireplaceHorseToken extends TokenImpl {
public TheGirlInTheFireplaceHorseToken() {
super("Horse Token", "2/2 white Horse creature token with \"Doctors you control have horsemanship.\"");
cardType.add(CardType.CREATURE);
color.setWhite(true);
subtype.add(SubType.HORSE);
power = new MageInt(2);
toughness = new MageInt(2);
this.addAbility(new SimpleStaticAbility(new GainAbilityControlledEffect(HorsemanshipAbility.getInstance(),
Duration.WhileOnBattlefield, new FilterCreaturePermanent(SubType.DOCTOR, "Doctors"), false)));
}
private TheGirlInTheFireplaceHorseToken(final TheGirlInTheFireplaceHorseToken token) {
super(token);
}
@Override
public TheGirlInTheFireplaceHorseToken copy() {
return new TheGirlInTheFireplaceHorseToken(this);
}
}

View file

@ -0,0 +1,40 @@
package mage.game.permanent.token;
import mage.MageInt;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.PreventDamageToSourceEffect;
import mage.abilities.keyword.VanishingAbility;
import mage.constants.CardType;
import mage.constants.SubType;
import mage.constants.Duration;
/**
* @author padfoot
*/
public final class TheGirlInTheFireplaceHumanNobleToken extends TokenImpl {
public TheGirlInTheFireplaceHumanNobleToken() {
super("Human Noble Token", "1/1 white Human Noble creature token with vanishing 3 and \"Prevent all damage that would be dealt to this creature.\"");
cardType.add(CardType.CREATURE);
color.setWhite(true);
subtype.add(SubType.HUMAN,SubType.NOBLE);
power = new MageInt(1);
toughness = new MageInt(1);
this.addAbility(new VanishingAbility(3));
this.addAbility(new SimpleStaticAbility(
new PreventDamageToSourceEffect(
Duration.WhileOnBattlefield,
Integer.MAX_VALUE
).setText("Prevent all damage that would be dealt to this creature.")
));
}
private TheGirlInTheFireplaceHumanNobleToken(final TheGirlInTheFireplaceHumanNobleToken token) {
super(token);
}
@Override
public TheGirlInTheFireplaceHumanNobleToken copy() {
return new TheGirlInTheFireplaceHumanNobleToken(this);
}
}

View file

@ -0,0 +1,30 @@
package mage.game.permanent.token;
import mage.MageInt;
import mage.constants.CardType;
import mage.constants.SubType;
/**
* @author TheElk801
*/
public final class ZombieDruidToken extends TokenImpl {
public ZombieDruidToken() {
super("Zombie Druid Token", "2/2 black Zombie Druid creature token");
cardType.add(CardType.CREATURE);
color.setBlack(true);
subtype.add(SubType.ZOMBIE);
subtype.add(SubType.DRUID);
power = new MageInt(2);
toughness = new MageInt(2);
}
private ZombieDruidToken(final ZombieDruidToken token) {
super(token);
}
@Override
public ZombieDruidToken copy() {
return new ZombieDruidToken(this);
}
}

View file

@ -3,7 +3,6 @@ package mage.game.turn;
import mage.abilities.Ability;
import mage.constants.PhaseStep;
import mage.constants.TurnPhase;
import mage.counters.CounterType;
import mage.game.Game;
import mage.game.events.PhaseChangedEvent;
import mage.game.permanent.Permanent;
@ -17,6 +16,7 @@ import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* @author BetaSteward_at_googlemail.com
@ -53,13 +53,6 @@ public class Turn implements Serializable {
}
public TurnPhase getPhaseType() {
if (currentPhase != null) {
return currentPhase.getType();
}
return null;
}
public Phase getPhase() {
return currentPhase;
}
@ -85,14 +78,9 @@ public class Turn implements Serializable {
}
/**
* @param game
* @param activePlayer
* @return true if turn is skipped
*/
public boolean play(Game game, Player activePlayer) {
// uncomment this to trace triggered abilities and/or continous effects
// TraceUtil.traceTriggeredAbilities(game);
// game.getState().getContinuousEffects().traceContinuousEffects(game);
activePlayer.becomesActivePlayer();
this.setDeclareAttackersStepStarted(false);
if (game.isPaused() || game.checkIfGameIsOver()) {
@ -150,7 +138,12 @@ public class Turn implements Serializable {
game.saveState(false);
//20091005 - 500.8
while (playExtraPhases(game, phase.getType())) ;
while (true) {
// TODO: make sure it work fine (without freeze) on game errors inside extra phases
if (!playExtraPhases(game, phase.getType())) {
break;
}
}
}
return false;
}
@ -158,7 +151,6 @@ public class Turn implements Serializable {
public void resumePlay(Game game, boolean wasPaused) {
activePlayerId = game.getActivePlayerId();
Player activePlayer = game.getPlayer(activePlayerId);
UUID priorityPlayerId = game.getPriorityPlayerId();
TurnPhase needPhaseType = game.getTurnPhaseType();
PhaseStep needStepType = game.getTurnStepType();
@ -259,10 +251,16 @@ public class Turn implements Serializable {
}
}
/**
* Play additional phases one by one
*
* @return false to finish
*/
private boolean playExtraPhases(Game game, TurnPhase afterPhase) {
while (true) {
TurnMod extraPhaseMod = game.getState().getTurnMods().useNextExtraPhase(activePlayerId, afterPhase);
if (extraPhaseMod == null) {
// no more extra phases
return false;
}
TurnPhase extraPhase = extraPhaseMod.getExtraPhase();
@ -316,12 +314,8 @@ public class Turn implements Serializable {
/**
* Used for some spells with end turn effect (e.g. Time Stop).
*
* @param game
* @param activePlayerId
* @param source
*/
public void endTurn(Game game, UUID activePlayerId, Ability source) {
public void endTurn(Game game, Ability source) {
// Ending the turn this way (Time Stop) means the following things happen in order:
setEndTurnRequested(true);
@ -391,26 +385,18 @@ public class Turn implements Serializable {
}
private void logStartOfTurn(Game game, Player player) {
StringBuilder sb = new StringBuilder("Turn ");
sb.append(game.getState().getTurnNum()).append(' ');
if (game.getState().isExtraTurn()) {
sb.append("(extra) ");
}
sb.append(player.getLogName());
sb.append(" (");
int delimiter = game.getPlayers().size() - 1;
for (Player gamePlayer : game.getPlayers().values()) {
sb.append(gamePlayer.getLife());
int poison = gamePlayer.getCountersCount(CounterType.POISON);
if (poison > 0) {
sb.append("[P:").append(poison).append(']');
}
if (delimiter > 0) {
sb.append(" - ");
delimiter--;
}
}
sb.append(')');
game.fireStatusEvent(sb.toString(), true, false);
// example: 0:40: TURN 1 for Human (40 - 40)
String infoTurn = String.format("TURN %d%s for %s",
game.getState().getTurnNum(),
game.getState().isExtraTurn() ? " (extra)" : "",
player.getLogName()
);
String infoLife = game.getPlayers().values().stream()
.map(p -> String.valueOf(p.getLife()))
.collect(Collectors.joining(" - "));
game.fireStatusEvent(infoTurn + " (" + infoLife + ")", true, false);
}
}

View file

@ -1,85 +0,0 @@
package mage.util.trace;
import java.util.UUID;
import mage.constants.Duration;
/**
*
* @author LevelX2
*/
public class TraceInfo {
public String info;
public String playerName;
public String sourceName;
public String rule;
public UUID abilityId;
public UUID effectId;
public Duration duration;
public long order;
public String getPlayerName() {
return playerName;
}
public void setPlayerName(String playerName) {
this.playerName = playerName;
}
public String getSourceName() {
return sourceName;
}
public void setSourceName(String sourceName) {
this.sourceName = sourceName;
}
public String getRule() {
return rule;
}
public void setRule(String rule) {
this.rule = rule;
}
public UUID getAbilityId() {
return abilityId;
}
public void setAbilityId(UUID abilityId) {
this.abilityId = abilityId;
}
public UUID getEffectId() {
return effectId;
}
public void setEffectId(UUID effectId) {
this.effectId = effectId;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
public Duration getDuration() {
return duration;
}
public void setDuration(Duration duration) {
this.duration = duration;
}
public long getOrder() {
return order;
}
public void setOrder(long order) {
this.order = order;
}
}

View file

@ -1,241 +0,0 @@
package mage.util.trace;
import java.util.*;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.StaticAbility;
import mage.abilities.TriggeredAbility;
import mage.abilities.effects.ContinuousEffectsList;
import mage.abilities.effects.RestrictionEffect;
import mage.abilities.keyword.CantBeBlockedSourceAbility;
import mage.abilities.keyword.FlyingAbility;
import mage.abilities.keyword.IntimidateAbility;
import mage.abilities.keyword.ReachAbility;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.combat.Combat;
import mage.game.combat.CombatGroup;
import mage.game.permanent.Permanent;
import mage.players.Player;
import org.apache.log4j.Logger;
/**
* @author magenoxx_at_gmail.com
*/
public final class TraceUtil {
private static final Logger log = Logger.getLogger(TraceUtil.class);
/**
* This method is intended to catch various bugs with combat.
*
* One of them (possibly the most annoying) is when creature without flying or reach blocks creature with flying.
* No test managed to reproduce it, but it happens in the games time to time and was reported by different players.
*
* The idea: is to catch such cases manually and print out as much information from game state that may help as possible.
* @param game
* @param combat
*/
public static void traceCombatIfNeeded(Game game, Combat combat) {
// trace non-flying vs flying
for (CombatGroup group : combat.getGroups()) {
for (UUID attackerId : group.getAttackers()) {
Permanent attacker = game.getPermanent(attackerId);
if (attacker != null) {
if (hasFlying(attacker)) {
// traceCombat(game, attacker, null);
for (UUID blockerId : group.getBlockers()) {
Permanent blocker = game.getPermanent(blockerId);
if (blocker != null && !hasFlying(blocker) && !hasReach(blocker)) {
log.warn("Found non-flying non-reach creature blocking creature with flying");
traceCombat(game, attacker, blocker);
}
}
}
if (hasIntimidate(attacker)) {
for (UUID blockerId : group.getBlockers()) {
Permanent blocker = game.getPermanent(blockerId);
if (blocker != null && !blocker.isArtifact(game)
&& !attacker.getColor(game).shares(blocker.getColor(game))) {
log.warn("Found creature with intimidate blocked by non artifact not sharing color creature");
traceCombat(game, attacker, blocker);
}
}
}
if (cantBeBlocked(attacker)) {
if (!group.getBlockers().isEmpty()) {
Permanent blocker = game.getPermanent(group.getBlockers().get(0));
if (blocker != null) {
log.warn("Found creature that can't be blocked by some other creature");
traceCombat(game, attacker, blocker);
}
}
}
}
}
}
}
/**
* We need this to check Flying existence in not-common way: by instanceof.
* @return
*/
private static boolean hasFlying(Permanent permanent) {
for (Ability ability : permanent.getAbilities()) {
if (ability instanceof FlyingAbility) {
return true;
}
}
return false;
}
private static boolean hasIntimidate(Permanent permanent) {
for (Ability ability : permanent.getAbilities()) {
if (ability instanceof IntimidateAbility) {
return true;
}
}
return false;
}
private static boolean hasReach(Permanent permanent) {
for (Ability ability : permanent.getAbilities()) {
if (ability instanceof ReachAbility) {
return true;
}
}
return false;
}
private static boolean cantBeBlocked(Permanent permanent) {
for (Ability ability : permanent.getAbilities()) {
if (ability instanceof CantBeBlockedSourceAbility) {
return true;
}
}
return false;
}
private static void traceCombat(Game game, Permanent attacker, Permanent blocker) {
String prefix = "> ";
log.error(prefix+"Tracing game state...");
if (blocker != null) {
log.error(prefix+blocker.getLogName() + " could block " + attacker.getLogName());
}
log.error(prefix);
log.error(prefix+"Attacker abilities: ");
for (Ability ability : attacker.getAbilities()) {
log.error(prefix+" " + ability.toString() + ", id=" + ability.getId());
}
if (blocker != null) {
log.error(prefix+"Blocker abilities: ");
for (Ability ability : blocker.getAbilities()) {
log.error(prefix+" " + ability.toString() + ", id=" + ability.getId());
}
}
log.error(prefix);
log.error(prefix+"Flying ability id: " + FlyingAbility.getInstance().getId());
log.error(prefix+"Reach ability id: " + ReachAbility.getInstance().getId());
log.error(prefix+"Intimidate ability id: " + IntimidateAbility.getInstance().getId());
log.error(prefix);
log.error(prefix+"Restriction effects:");
log.error(prefix+" Applied to ATTACKER:");
Map<RestrictionEffect, Set<Ability>> attackerResEffects = game.getContinuousEffects().getApplicableRestrictionEffects(attacker, game);
for (Map.Entry<RestrictionEffect, Set<Ability>> entry : attackerResEffects.entrySet()) {
log.error(prefix+" " + entry.getKey());
log.error(prefix+" id=" + entry.getKey().getId());
for (Ability ability: entry.getValue()) {
log.error(prefix+" ability=" + ability);
}
}
log.error(prefix+" Applied to BLOCKER:");
if (blocker != null) {
Map<RestrictionEffect, Set<Ability>> blockerResEffects = game.getContinuousEffects().getApplicableRestrictionEffects(blocker, game);
for (Map.Entry<RestrictionEffect, Set<Ability>> entry : blockerResEffects.entrySet()) {
log.error(prefix+" " + entry.getKey());
log.error(prefix+" id=" + entry.getKey().getId());
for (Ability ability: entry.getValue()) {
log.error(prefix+" ability=" + ability);
}
}
}
ContinuousEffectsList<RestrictionEffect> restrictionEffects = (ContinuousEffectsList<RestrictionEffect>) game.getContinuousEffects().getRestrictionEffects();
log.error(prefix);
log.error(prefix+" List of all restriction effects:");
for (RestrictionEffect effect : restrictionEffects) {
log.error(prefix+" " + effect);
log.error(prefix+" id=" + effect.getId());
}
log.error(prefix);
log.error(prefix+" Trace Attacker:");
traceForPermanent(game, attacker, prefix, restrictionEffects);
if (blocker != null) {
log.error(prefix);
log.error(prefix+" Trace Blocker:");
traceForPermanent(game, blocker, prefix, restrictionEffects);
}
log.error(prefix);
}
private static void traceForPermanent(Game game, Permanent permanent, String uuid, ContinuousEffectsList<RestrictionEffect> restrictionEffects) {
for (RestrictionEffect effect: restrictionEffects) {
log.error(uuid+" effect=" + effect.toString() + " id=" + effect.getId());
for (Ability ability : restrictionEffects.getAbility(effect.getId())) {
if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, permanent, null)) {
log.error(uuid+" ability=" + ability + ", applies_to_attacker=" + effect.applies(permanent, ability, game));
} else {
boolean usable = ability.isInUseableZone(game, permanent, null);
log.error(uuid+" instanceof StaticAbility: " + (ability instanceof StaticAbility) + ", ability=" + ability);
log.error(uuid+" usable zone: " + usable + ", ability=" + ability);
if (!usable) {
Zone zone = ability.getZone();
log.error(uuid+" zone: " + zone);
MageObject object = game.getObject(ability.getSourceId());
log.error(uuid+" object: " + object);
if (object != null) {
log.error(uuid + " contains ability: " + object.getAbilities().contains(ability));
}
Zone test = game.getState().getZone(ability.getSourceId());
log.error(uuid+" test_zone: " + test);
}
}
}
}
}
public static void trace(String msg) {
log.info(msg);
}
/**
* Prints out a status of the currently existing triggered abilities
* @param game
*/
public static void traceTriggeredAbilities(Game game) {
log.info("-------------------------------------------------------------------------------------------------");
log.info("Turn: " + game.getTurnNum() + " - currently existing triggered abilities: " + game.getState().getTriggers().size());
Map<String, String> orderedAbilities = new TreeMap<>();
for (Map.Entry<String, TriggeredAbility> entry : game.getState().getTriggers().entrySet()) {
Player controller = game.getPlayer(entry.getValue().getControllerId());
MageObject source = game.getObject(entry.getValue().getSourceId());
orderedAbilities.put((controller == null ? "no controller": controller.getName()) + (source == null ? "no source": source.getIdName())+ entry.getKey(), entry.getKey());
}
String playerName = "";
for (Map.Entry<String, String> entry : orderedAbilities.entrySet()) {
TriggeredAbility trAbility = game.getState().getTriggers().get(entry.getValue());
Player controller = game.getPlayer(trAbility.getControllerId());
MageObject source = game.getObject(trAbility.getSourceId());
if (!controller.getName().equals(playerName)) {
playerName = controller.getName();
log.info("--- Player: " + playerName + " --------------------------------");
}
log.info((source == null ? "no source": source.getIdName()) + " -> "
+ trAbility.getRule());
}
}
}