Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
1481a65
Create DanDan game and deck types
agylesox Mar 22, 2026
7fe42b3
Create DanDan game and deck types
agylesox Mar 22, 2026
6ec7f68
create folder for dandan decks and link to gui
agylesox Mar 22, 2026
6bf621c
selecting the variant dandan in the constructed game menu defaults th…
agylesox Mar 23, 2026
236a847
Added deck type drop down list to deck editor, so each format's rules…
agylesox Mar 23, 2026
30c5237
Deck Editor enforces deck construction rules based on format
agylesox Mar 23, 2026
21ed9bf
Added Secret Lair version of DanDan.dck
agylesox Mar 23, 2026
fcaee89
Added deck description to deck editor
agylesox Mar 23, 2026
43e4a1d
Increase size of deck description field in the deck editor
agylesox Mar 23, 2026
f70e1b2
DanDan players share a library
agylesox Mar 23, 2026
832309b
Update metadata for DanDan decks
agylesox Mar 23, 2026
3e799fb
added verbose logging for simulation runs
agylesox Mar 23, 2026
244e7b8
added a verbose logging config file
agylesox Mar 23, 2026
189281c
added beginning hand logging as verbose config
agylesox Mar 23, 2026
32ebdd0
updated verbose config file locations search
agylesox Mar 23, 2026
7279756
updated verbose config file for showing first n cards of library at b…
agylesox Mar 23, 2026
12d8309
updated verbose config file for showing first n cards of library at b…
agylesox Mar 23, 2026
45e7363
verbose logging now uses card name and id instead of just card name
agylesox Mar 23, 2026
fb47f71
updated view all cards dev mode to flip the cards face up when viewin…
agylesox Mar 23, 2026
d4a054b
atttempt to fix gui showing correct library state
agylesox Mar 24, 2026
ebe372f
added verbose graveyard logging
agylesox Mar 24, 2026
7e32280
added dandantest deck that scries and discards cards so shared librar…
agylesox Mar 24, 2026
17e13cc
update sim verbose exmaple properties to include graveyard logging
agylesox Mar 24, 2026
990c822
Shared library is working, drawer of card becomes owner of card so AI…
agylesox Mar 26, 2026
5382dcf
add dandanviewzones
agylesox Mar 26, 2026
4ca8029
fixed shared graveyard
agylesox Mar 26, 2026
7f3148f
fixed shared graveyard for controller
agylesox Mar 26, 2026
eca0933
fixed shared graveyard for controller
agylesox Mar 27, 2026
d366ebe
added more dandan decks
agylesox Mar 27, 2026
324f651
add separate default layout for dandan
agylesox Mar 28, 2026
a53ae34
fix match_dandan.xml
agylesox Mar 28, 2026
1a9207c
hand_1 now available to UI on dandan match start and small dandan xml…
agylesox Mar 28, 2026
19c5aa6
remove verbose logger
agylesox Mar 30, 2026
8bbb723
update gitignore
agylesox Mar 30, 2026
a206eae
go back to using .dck comment field instead of description. updates t…
agylesox Mar 31, 2026
f98f5a1
use GameRules to enforce dandan like how commander is enforced
agylesox Mar 31, 2026
c6febd5
simplify dandan case for random deck generator
agylesox Mar 31, 2026
08d7948
updated deck comment for two dandan decks
agylesox Mar 31, 2026
5b35c5b
hover over name in deck selector now displays comment from .dck. also…
agylesox Mar 31, 2026
267ceb9
hover over name in deck selector now displays comment from .dck. also…
agylesox Mar 31, 2026
4016e6c
Merge remote-tracking branch 'upstream/master'
agylesox Mar 31, 2026
5b6f8e9
update gitignore for branch refactor
agylesox Apr 1, 2026
734be2b
Merge remote-tracking branch 'upstream/master'
agylesox Apr 1, 2026
7880766
Merge branch 'Card-Forge:master' into master
agylesox Apr 4, 2026
48f4fc5
refactor dandan and clean up
agylesox Apr 5, 2026
0abb4b1
Merge branch 'master' of github.com:agylesox/forge
agylesox Apr 5, 2026
a4fa403
Merge remote-tracking branch 'upstream/master'
agylesox Apr 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,4 @@ __pycache__
*.pyc

# Ignore Claude configuration
.claude
.claude
10 changes: 10 additions & 0 deletions forge-core/src/main/java/forge/deck/DeckBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public abstract class DeckBase implements Serializable, Comparable<DeckBase>, In
private String name;
private transient String directory;
private String comment = null;
private DeckFormat deckFormat = DeckFormat.Constructed;

/**
* Instantiates a new deck base.
Expand Down Expand Up @@ -116,6 +117,14 @@ public String getComment() {
return comment;
}

public DeckFormat getDeckFormat() {
return deckFormat;
}

public void setDeckFormat(final DeckFormat deckFormat0) {
deckFormat = deckFormat0 == null ? DeckFormat.Constructed : deckFormat0;
}

/**
* New instance.
*
Expand All @@ -132,6 +141,7 @@ public String getComment() {
protected void cloneFieldsTo(final DeckBase clone) {
clone.directory = directory;
clone.comment = comment;
clone.deckFormat = deckFormat;
}

/**
Expand Down
2 changes: 2 additions & 0 deletions forge-core/src/main/java/forge/deck/DeckFormat.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
public enum DeckFormat {
// Main board: allowed size SB: restriction Max distinct non-basic cards
Constructed ( Range.of(60, Integer.MAX_VALUE), Range.of(0, 15), 4),
/** DanDan decks do not use the default 4-of restriction. */
DanDan ( Range.of(60, Integer.MAX_VALUE), Range.of(0, 15), Integer.MAX_VALUE),
QuestDeck ( Range.of(40, Integer.MAX_VALUE), Range.of(0, 15), 4),
Limited ( Range.of(40, Integer.MAX_VALUE), null, Integer.MAX_VALUE) {
@Override
Expand Down
2 changes: 2 additions & 0 deletions forge-core/src/main/java/forge/deck/io/DeckSerializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ private static List<String> serializeDeck(Deck d) {
out.add(TextUtil.enclosedBracket("metadata"));

out.add(TextUtil.concatNoSpace(DeckFileHeader.NAME,"=", d.getName().replaceAll("\n", "")));
out.add(TextUtil.concatNoSpace(DeckFileHeader.DECK_TYPE, "=", d.getDeckFormat().name()));
// these are optional
if (d.getComment() != null) {
out.add(TextUtil.concatNoSpace(DeckFileHeader.COMMENT,"=", d.getComment().replaceAll("\n", "")));
Expand Down Expand Up @@ -99,6 +100,7 @@ public static Deck fromSections(final Map<String, List<String>> sections) {
}

Deck d = new Deck(dh.getName());
d.setDeckFormat(dh.getDeckType());
d.setComment(dh.getComment());
d.setAiHints(dh.getAiHints());
d.getTags().addAll(dh.getTags());
Expand Down
119 changes: 119 additions & 0 deletions forge-game/src/main/java/forge/game/DanDanViewZones.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package forge.game;

import java.util.HashSet;

import forge.card.CardType;
import forge.game.card.Card;
import forge.game.card.CardView;
import forge.game.player.Player;
import forge.game.player.PlayerView;
import forge.game.zone.PlayerZone;
import forge.game.zone.ZoneType;
import forge.trackable.TrackableProperty;
import forge.util.collect.FCollectionView;

/**
* Canonical {@link PlayerView} source for DanDan shared {@link ZoneType#Library} and
* {@link ZoneType#Graveyard}: each seat may carry its own trackable copy, but UI and verbose tooling
* should use the first registered player's lists so order and counts match the engine and sim.
*/
public final class DanDanViewZones {

private DanDanViewZones() {
}

/** Prefer live {@link Game} rules when present so UI matches sim even if trackable {@link GameType} lags. */
public static boolean isDanDan(final GameView gameView) {
if (gameView == null) {
return false;
}
final Game g = gameView.getGame();
if (g != null) {
if (g.getRules() != null && g.getRules().isDanDan()) {
return true;
}
}
final Match match = gameView.getMatch();
if (match != null) {
if (match.getRules() != null && match.getRules().isDanDan()) {
return true;
}
}
return gameView.getGameType() == GameType.DanDan;
}

/**
* Card list to show for {@code player} in {@code zone}. For DanDan library/graveyard, returns the
* first player's list.
*/
public static FCollectionView<CardView> cardsForZoneDisplay(final GameView gameView, final PlayerView player,
final ZoneType zone) {
if (gameView != null && isDanDan(gameView)
&& (zone == ZoneType.Library || zone == ZoneType.Graveyard)) {
// Prefer live model zone order when available (desktop local games).
final Game g = gameView.getGame();
if (g != null && !g.getPlayers().isEmpty()) {
final Player canonical = g.getPlayers().get(0);
final PlayerZone sharedZone = canonical.getZone(zone);
if (sharedZone != null) {
final Iterable<Card> liveCards = sharedZone.getCards(false);
return CardView.getCollection(liveCards);
}
}

final FCollectionView<PlayerView> players = gameView.getPlayers();
if (players != null && !players.isEmpty()) {
final FCollectionView<CardView> shared = players.get(0).getCards(zone);
if (shared != null) {
return shared;
}
}
}
return player == null ? null : player.getCards(zone);
}

/** Count aligned with {@link #cardsForZoneDisplay}. */
public static int zoneCountForDisplay(final GameView gameView, final PlayerView player, final ZoneType zone) {
final FCollectionView<CardView> cards = cardsForZoneDisplay(gameView, player, zone);
return cards == null ? 0 : cards.size();
}

/**
* Distinct card types in graveyard for UI (e.g. tooltips, delirium tint); uses canonical list in DanDan.
*/
public static int graveyardTypeCountForDisplay(final GameView gameView, final PlayerView player) {
if (gameView != null && isDanDan(gameView)) {
final FCollectionView<CardView> cards = cardsForZoneDisplay(gameView, player, ZoneType.Graveyard);
if (cards == null) {
return 0;
}
final HashSet<CardType.CoreType> types = new HashSet<>();
for (final CardView c : cards) {
types.addAll(c.getCurrentState().getType().getCoreTypes());
}
return types.size();
}
return player == null ? 0 : player.getZoneTypes(TrackableProperty.Graveyard);
}

/** Delirium highlight in zone tabs; uses canonical graveyard in DanDan. */
public static boolean hasDeliriumForDisplay(final GameView gameView, final PlayerView player) {
if (player == null) {
return false;
}
return graveyardTypeCountForDisplay(gameView, player) >= 4;
}

private static String shortIdHash(final Iterable<CardView> cards) {
if (cards == null) {
return "null";
}
int count = 0;
int hash = 1;
for (final CardView c : cards) {
count++;
hash = 31 * hash + (c == null ? 0 : c.getId());
}
return count + ":" + Integer.toHexString(hash);
}
}
10 changes: 9 additions & 1 deletion forge-game/src/main/java/forge/game/GameAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -881,7 +881,15 @@ public final Card moveToLibrary(Card c, int libPosition, SpellAbility cause) {
return moveToLibrary(c, libPosition, cause, null);
}
public final Card moveToLibrary(Card c, int libPosition, SpellAbility cause, Map<AbilityKey, Object> params) {
final PlayerZone library = c.getOwner().getZone(ZoneType.Library);
final PlayerZone library;
final GameRules rules = game.getRules();
final boolean isDanDan = rules != null && rules.isDanDan();
if (isDanDan && !game.getPlayers().isEmpty()) {
// DanDan uses one shared library zone for all players.
library = game.getPlayers().get(0).getZone(ZoneType.Library);
} else {
library = c.getOwner().getZone(ZoneType.Library);
}
if (libPosition == -1 || libPosition > library.size()) {
libPosition = library.size();
}
Expand Down
43 changes: 39 additions & 4 deletions forge-game/src/main/java/forge/game/GameRules.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package forge.game;

import forge.game.zone.Zone;
import forge.game.zone.ZoneType;

import java.util.EnumSet;
import java.util.Set;

Expand Down Expand Up @@ -113,11 +116,43 @@ public boolean hasAppliedVariant(final GameType variant) {
return appliedVariants.contains(variant);
}

public boolean isTypeOrVariant(final GameType type) {
return gameType == type || hasAppliedVariant(type);
}

public boolean isDanDan() {
return isTypeOrVariant(GameType.DanDan);
}

/**
* When true, card-property and related activation logic should not fail strictly on
* per-player controller or ownership for the given zone. Callers pass the card's
* current zone or last-known zone as appropriate.
* <p>
* Today this applies to DanDan's shared graveyard only; additional variants or zones
* can be folded into the implementation without changing the method name.
* </p>
*
* @param zoneType the zone type to evaluate, or null (treated as not relaxed)
* @return whether relaxed controller/ownership checks apply for card properties
*/
public boolean relaxesControllerOwnershipForCardProperties(final ZoneType zoneType) {
return isDanDan() && zoneType == ZoneType.Graveyard;
}

/**
* @param zone the zone to evaluate, or null (treated as not relaxed)
* @see #relaxesControllerOwnershipForCardProperties(ZoneType)
*/
public boolean relaxesControllerOwnershipForCardProperties(final Zone zone) {
return zone != null && relaxesControllerOwnershipForCardProperties(zone.getZoneType());
}

public boolean hasCommander() {
return appliedVariants.contains(GameType.Commander)
|| appliedVariants.contains(GameType.Oathbreaker)
|| appliedVariants.contains(GameType.TinyLeaders)
|| appliedVariants.contains(GameType.Brawl);
return isTypeOrVariant(GameType.Commander)
|| isTypeOrVariant(GameType.Oathbreaker)
|| isTypeOrVariant(GameType.TinyLeaders)
|| isTypeOrVariant(GameType.Brawl);
}

public boolean useGrayText() {
Expand Down
3 changes: 2 additions & 1 deletion forge-game/src/main/java/forge/game/GameType.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public enum GameType {
AdventureEvent (DeckFormat.Limited, true, true, true, "lblAdventure", ""),
Puzzle (DeckFormat.Puzzle, false, false, false, "lblPuzzle", "lblPuzzleDesc"),
Constructed (DeckFormat.Constructed, false, true, true, "lblConstructed", ""),
DanDan (DeckFormat.DanDan, false, true, true, "lblDanDan", "lblDanDanDesc"),
DeckManager (DeckFormat.Constructed, false, true, true, "lblDeckManager", ""),
Vanguard (DeckFormat.Vanguard, true, true, true, "lblVanguard", "lblVanguardDesc"),
Commander (DeckFormat.Commander, false, false, false, "lblCommander", "lblCommanderDesc"),
Expand Down Expand Up @@ -158,7 +159,7 @@ public EnumSet<DeckSection> getSupplimentalDeckSections() {
return EnumSet.noneOf(DeckSection.class); //Already an extra deck, like a dedicated Scheme or Planar deck.
if(deckFormat == DeckFormat.Limited)
return EnumSet.of(DeckSection.Conspiracy, DeckSection.Contraptions, DeckSection.Attractions);
if(this == Constructed || this == Commander)
if(this == Constructed || this == Commander || this == DanDan)
return EnumSet.of(DeckSection.Avatar, DeckSection.Schemes, DeckSection.Planes, DeckSection.Conspiracy,
DeckSection.Attractions, DeckSection.Contraptions);
return EnumSet.of(DeckSection.Attractions, DeckSection.Contraptions);
Expand Down
22 changes: 19 additions & 3 deletions forge-game/src/main/java/forge/game/Match.java
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,14 @@ private void prepareAllZones(final Game game) {

final FCollectionView<Player> players = game.getPlayers();
final List<RegisteredPlayer> playersConditions = game.getMatch().getPlayers();
final boolean isDanDan = rules.isDanDan() && !players.isEmpty();
final Player sharedDanDanPlayer = isDanDan ? players.get(0) : null;
final RegisteredPlayer sharedDanDanCondition = isDanDan && !playersConditions.isEmpty() ? playersConditions.get(0) : null;

boolean isFirstGame = gameOutcomes.isEmpty();
boolean canSideBoard = !isFirstGame && rules.getGameType().isSideboardingAllowed();
// DanDan has a shared library/graveyard but no "between-games" sideboarding.
// Prevent sideboarding logic from ever running for DanDan matches.
boolean canSideBoard = !isFirstGame && rules.getGameType().isSideboardingAllowed() && !isDanDan;
// Only allow this if feature flag is on AND for certain match types
boolean sideboardForAIs = rules.getSideboardForAI() &&
rules.getGameType().getDeckFormat().equals(DeckFormat.Constructed);
Expand All @@ -250,6 +255,9 @@ private void prepareAllZones(final Game game) {
for (int i = 0; i < playersConditions.size(); i++) {
final Player player = players.get(i);
final RegisteredPlayer psc = playersConditions.get(i);
if (isDanDan && i > 0) {
psc.useSharedDeckFrom(sharedDanDanCondition);
}
PlayerController person = player.getController();

if (canSideBoard) {
Expand Down Expand Up @@ -313,7 +321,12 @@ private void prepareAllZones(final Game game) {
}
}

preparePlayerZone(player, ZoneType.Library, myDeck.getLeft().getMain(), psc.useRandomFoil());
if (!isDanDan || i == 0) {
preparePlayerZone(player, ZoneType.Library, myDeck.getLeft().getMain(), psc.useRandomFoil());
} else {
player.useSharedZoneFrom(sharedDanDanPlayer, ZoneType.Library);
player.useSharedZoneFrom(sharedDanDanPlayer, ZoneType.Graveyard);
}
if (myDeck.getLeft().has(DeckSection.Sideboard)) {
preparePlayerZone(player, ZoneType.Sideboard, myDeck.getLeft().get(DeckSection.Sideboard), psc.useRandomFoil());

Expand All @@ -322,7 +335,10 @@ private void prepareAllZones(final Game game) {

player.initVariantsZones(psc);

player.shuffle(null);
//Necessary to prevent duplicating shuffle events (and triggers to shuffle listeners) in DanDan setup
if (!isDanDan || i == 0) {
player.shuffle(null);
}

if (isFirstGame) {
Map<DeckSection, List<? extends PaperCard>> cardsComplained = player.getController().complainCardsCantPlayWell(myDeck.getLeft());
Expand Down
21 changes: 12 additions & 9 deletions forge-game/src/main/java/forge/game/card/CardProperty.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public class CardProperty {
public static boolean cardHasProperty(Card card, String property, Player sourceController, Card source, CardTraitBase spellAbility) {
final Game game = card.getGame();
final Combat combat = game.getCombat();
final boolean useRelaxedOwnershipForCardProperties = game != null
&& game.getRules().relaxesControllerOwnershipForCardProperties(card.getZone());
// lki can't be null but it does return this
final Card lki = game.getChangeZoneLKIInfo(card);
final Player controller = lki.getController();
Expand Down Expand Up @@ -174,19 +176,19 @@ public static boolean cardHasProperty(Card card, String property, Player sourceC
return false;
}
} else if (property.startsWith("YouCtrl")) {
if (!controller.equals(sourceController)) {
if (!controller.equals(sourceController) && !useRelaxedOwnershipForCardProperties) {
return false;
}
} else if (property.startsWith("YourTeamCtrl")) {
if (controller.getTeam() != sourceController.getTeam()) {
if (controller.getTeam() != sourceController.getTeam() && !useRelaxedOwnershipForCardProperties) {
return false;
}
} else if (property.startsWith("YouDontCtrl")) {
if (controller.equals(sourceController)) {
if (controller.equals(sourceController) && !useRelaxedOwnershipForCardProperties) {
return false;
}
} else if (property.startsWith("OppCtrl")) {
if (!controller.getOpponents().contains(sourceController)) {
if (!controller.getOpponents().contains(sourceController) && !useRelaxedOwnershipForCardProperties) {
return false;
}
} else if (property.startsWith("ChosenCtrl")) {
Expand Down Expand Up @@ -283,24 +285,25 @@ public static boolean cardHasProperty(Card card, String property, Player sourceC
return false;
}
} else if (property.startsWith("YouOwn")) {
if (!card.getOwner().equals(sourceController)) {
if (!card.getOwner().equals(sourceController) && !useRelaxedOwnershipForCardProperties) {
return false;
}
} else if (property.startsWith("YouDontOwn")) {
if (card.getOwner().equals(sourceController)) {
if (card.getOwner().equals(sourceController) && !useRelaxedOwnershipForCardProperties) {
return false;
}
} else if (property.startsWith("OppOwn")) {
if (!card.getOwner().getOpponents().contains(sourceController)) {
if (!card.getOwner().getOpponents().contains(sourceController) && !useRelaxedOwnershipForCardProperties) {
return false;
}
} else if (property.equals("TargetedPlayerOwn")) {
if (!AbilityUtils.getDefinedPlayers(source, "TargetedPlayer", spellAbility).contains(card.getOwner())) {
if (!AbilityUtils.getDefinedPlayers(source, "TargetedPlayer", spellAbility).contains(card.getOwner())
&& !useRelaxedOwnershipForCardProperties) {
return false;
}
} else if (property.startsWith("OwnedBy")) {
final String valid = property.substring(8);
if (!card.getOwner().isValid(valid, sourceController, source, spellAbility)) {
if (!card.getOwner().isValid(valid, sourceController, source, spellAbility) && !useRelaxedOwnershipForCardProperties) {
final List<Player> lp = AbilityUtils.getDefinedPlayers(source, valid, spellAbility);
if (!lp.contains(card.getOwner())) {
return false;
Expand Down
Loading