Skip to content

Commit

Permalink
Dependency Graphs (#6869)
Browse files Browse the repository at this point in the history
  • Loading branch information
tool4ever authored Jan 28, 2025
1 parent 97553c5 commit 1e1fa1a
Show file tree
Hide file tree
Showing 8 changed files with 329 additions and 104 deletions.
1 change: 1 addition & 0 deletions forge-ai/src/main/java/forge/ai/simulation/GameCopier.java
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public Game makeCopy(PhaseType advanceToPhase, Player aiPlayer) {
GameRules currentRules = origGame.getRules();
Match newMatch = new Match(currentRules, newPlayers, origGame.getView().getTitle());
Game newGame = new Game(newPlayers, currentRules, newMatch);
newGame.dangerouslySetTimestamp(origGame.getTimestamp());

for (int i = 0; i < origGame.getPlayers().size(); i++) {
Player origPlayer = origGame.getPlayers().get(i);
Expand Down
8 changes: 7 additions & 1 deletion forge-game/pom.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

Expand Down Expand Up @@ -34,5 +35,10 @@
<artifactId>sentry-logback</artifactId>
<version>7.15.0</version>
</dependency>
<dependency>
<groupId>org.jgrapht</groupId>
<artifactId>jgrapht-core</artifactId>
<version>1.5.2</version>
</dependency>
</dependencies>
</project>
143 changes: 137 additions & 6 deletions forge-game/src/main/java/forge/game/GameAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityCantAttackBlock;
import forge.game.staticability.StaticAbilityContinuous;
import forge.game.staticability.StaticAbilityLayer;
import forge.game.trigger.TriggerType;
import forge.game.zone.PlayerZone;
Expand All @@ -56,6 +57,9 @@
import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.jgrapht.alg.cycle.SzwarcfiterLauerSimpleCycles;
import org.jgrapht.graph.DefaultDirectedGraph;
import org.jgrapht.graph.DefaultEdge;

import java.util.*;

Expand All @@ -70,6 +74,9 @@ public class GameAction {

private boolean holdCheckingStaticAbilities = false;

private final static Comparator<StaticAbility> effectOrder = Comparator.comparing(StaticAbility::isCharacteristicDefining).reversed()
.thenComparing(s -> s.getHostCard().getLayerTimestamp());

public GameAction(Game game0) {
game = game0;
}
Expand Down Expand Up @@ -1105,16 +1112,25 @@ public boolean visit(final Card c) {
}
}, true);

final Comparator<StaticAbility> comp = (a, b) -> ComparisonChain.start()
.compareTrueFirst(a.isCharacteristicDefining(), b.isCharacteristicDefining())
.compare(a.getHostCard().getLayerTimestamp(), b.getHostCard().getLayerTimestamp())
.result();
staticAbilities.sort(comp);
staticAbilities.sort(effectOrder);

final Map<StaticAbility, CardCollectionView> affectedPerAbility = Maps.newHashMap();
for (final StaticAbilityLayer layer : StaticAbilityLayer.CONTINUOUS_LAYERS) {
List<StaticAbility> toAdd = Lists.newArrayList();
for (final StaticAbility stAb : staticAbilities) {
List<StaticAbility> staticsForLayer = Lists.newArrayList();
for (StaticAbility stAb : staticAbilities) {
if (stAb.getLayers().contains(layer)) {
staticsForLayer.add(stAb);
}
}

while (!staticsForLayer.isEmpty()) {
StaticAbility stAb = staticsForLayer.get(0);
// dependency with CDA seems unlikely
if (!stAb.isCharacteristicDefining()) {
stAb = findStaticAbilityToApply(layer, staticsForLayer, preList, affectedPerAbility);
}
staticsForLayer.remove(stAb);
final CardCollectionView previouslyAffected = affectedPerAbility.get(stAb);
final CardCollectionView affectedHere;
if (previouslyAffected == null) {
Expand All @@ -1123,6 +1139,9 @@ public boolean visit(final Card c) {
affectedPerAbility.put(stAb, affectedHere);
}
} else {
// 613.6 If an effect starts to apply in one layer and/or sublayer, it will continue to be applied
// to the same set of objects in each other applicable layer and/or sublayer,
// even if the ability generating the effect is removed during this process.
affectedHere = previouslyAffected;
stAb.applyContinuousAbility(layer, previouslyAffected);
}
Expand All @@ -1136,6 +1155,9 @@ public boolean visit(final Card c) {
}
}
}
// 613.8c. After each effect is applied, the order of remaining effects is reevaluated
// and may change if an effect that has not yet been applied becomes
// dependent on or independent of one or more other effects that have not yet been applied.
}
staticAbilities.addAll(toAdd);
for (Player p : game.getPlayers()) {
Expand Down Expand Up @@ -1211,6 +1233,115 @@ public boolean visit(final Card c) {
game.getTracker().unfreeze();
}

private StaticAbility findStaticAbilityToApply(StaticAbilityLayer layer, List<StaticAbility> staticsForLayer, CardCollectionView preList, Map<StaticAbility, CardCollectionView> affectedPerAbility) {
if (staticsForLayer.size() == 1) {
return staticsForLayer.get(0);
}
if (!StaticAbilityLayer.CONTINUOUS_LAYERS_WITH_DEPENDENCY.contains(layer)) {
return staticsForLayer.get(0);
}

DefaultDirectedGraph<StaticAbility, DefaultEdge> dependencyGraph = new DefaultDirectedGraph<>(DefaultEdge.class);

for (StaticAbility stAb : staticsForLayer) {
dependencyGraph.addVertex(stAb);

boolean exists = stAb.getHostCard().getStaticAbilities().contains(stAb);
boolean compareAffected = true;
CardCollectionView affectedHere = affectedPerAbility.get(stAb);
if (affectedHere == null) {
affectedHere = StaticAbilityContinuous.getAffectedCards(stAb, preList);
} else {
compareAffected = false;
}
List<Object> effectResults = generateStaticAbilityResult(layer, stAb);

for (StaticAbility otherStAb : staticsForLayer) {
if (stAb == otherStAb) {
continue;
}

boolean removeFull = true;
CardCollectionView affectedOther = affectedPerAbility.get(otherStAb);
if (affectedOther == null) {
affectedOther = otherStAb.applyContinuousAbilityBefore(layer, preList);
if (affectedOther == null) {
// ability was removed
continue;
}
} else {
removeFull = false;
otherStAb.applyContinuousAbility(layer, affectedOther);
}

// 613.8a. An effect is said to "depend on" another if
// (b) applying the other would change the text or the existence of the first effect...
boolean dependency = exists != stAb.getHostCard().getStaticAbilities().contains(stAb);
// ...what it applies to...
if (!dependency && compareAffected) {
CardCollectionView affectedAfterOther = StaticAbilityContinuous.getAffectedCards(stAb, preList);
if (!Iterators.elementsEqual(affectedHere.iterator(), affectedAfterOther.iterator())) {
dependency = true;
}
}
// ...or what it does to any of the things it applies to
if (!dependency) {
List<Object> effectResultsAfterOther = generateStaticAbilityResult(layer, stAb);
if (!effectResults.equals(effectResultsAfterOther)) {
dependency = true;
}
}

if (dependency) {
dependencyGraph.addVertex(otherStAb);
dependencyGraph.addEdge(stAb, otherStAb);
}

// undo changes and check next pair
game.getStaticEffects().removeStaticEffect(otherStAb, layer, removeFull);
}
// when lucky the effect with the earliest timestamp has no dependency
// then we can safely return it - otherwise we need to build the whole graph
// because it might still be part of a loop
if (dependencyGraph.edgeSet().isEmpty() && stAb == staticsForLayer.get(0)) {
return stAb;
}
}

// 613.8b. If several dependent effects form a dependency loop, then this rule is ignored
List<List<StaticAbility>> cycles = new SzwarcfiterLauerSimpleCycles<>(dependencyGraph).findSimpleCycles();
for (List<StaticAbility> cyc : cycles) {
for (int i = 0 ; i < cyc.size() - 1 ; i++) {
dependencyGraph.removeEdge(cyc.get(i), cyc.get(i + 1));
}
// remove final edge
dependencyGraph.removeEdge(cyc.get(cyc.size() - 1), cyc.get(0));
}

// remove all effects that are still dependent on another
Set<StaticAbility> toRemove = Sets.newHashSet();
for (StaticAbility stAb : dependencyGraph.vertexSet()) {
if (dependencyGraph.outDegreeOf(stAb) > 0) {
toRemove.add(stAb);
}
}
dependencyGraph.removeAllVertices(toRemove);

// now the earlist one left is the correct choice
List<StaticAbility> statics = Lists.newArrayList(dependencyGraph.vertexSet());
statics.sort(Comparator.comparing(s -> s.getHostCard().getLayerTimestamp()));

return statics.get(0);
}

private List<Object> generateStaticAbilityResult(StaticAbilityLayer layer, StaticAbility stAb) {
List<Object> results = Lists.newArrayList();
if (layer == StaticAbilityLayer.CONTROL) {
results.addAll(AbilityUtils.getDefinedPlayers(stAb.getHostCard(), stAb.getParam("GainControl"), stAb));
}
return results;
}

public final boolean checkStateEffects(final boolean runEvents) {
return checkStateEffects(runEvents, Sets.newHashSet());
}
Expand Down
Loading

0 comments on commit 1e1fa1a

Please sign in to comment.