Commit cccaf176 authored by imakunee's avatar imakunee

Added Commander quest mode and world

 Added getFilteredPool() to easily get a Predicate applied copy of a CardPool.

 Minor formatting change.

 Added Random Commander to the list.

 New enum for defining the subformat a quest is using.

 getLife() now has a switch for modifying the life for sub-formats.

 New data save version. Includes a DeckConstructionRules enum.

 updateSaveFile will update old saves to have a default DeckConstructionRules complying with the new QuestData save version.

 Updated to include support for DeckConstructionRules and specialized duel managers

 Now have boolean to define if this is a "random" match for the duel list. Currently only QuestEventCommanderDuelManager makes use of this feature for Commander quests.

 New QuestEventDuel used in the QuestEventCommanderDuelManager which contains a DeckProxy for use in generating random commander decks.

 New duel manager to generate duels by difficulty for a Commander quest. Currently uses random generation to generate the decks of each opponent.

 Sell Extras button now has a switch for taking into account special deck construction rules such as Commander only allowing singletons.

 Starting a game now checks for various sub-format specific changes including a switch case for which variety of registered player to use.

 Starting cardpool size is now modified by a switch case for sub-formats such as Commander.

 QuestEvents marked as random matches will now award a "Random Opponent Bonus" equal to the credit base. Currently only QuestEventCommanderDuelManager creates QuestEvents marked as such.

 Added support for the Commander quest format and world.

 Many changes to add support for Commander in a style that, hopefully, also paths the way for future format support.

 Support for Commander quests.

 Support for Commander quests.
parent 03cd56d2
......@@ -17,6 +17,7 @@
package forge.deck;
import forge.StaticData;
import forge.card.CardDb;
......@@ -216,4 +217,17 @@ public class CardPool extends ItemPool<PaperCard> {
return sb.toString();
* Applies a predicate to this CardPool's cards.
* @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
public CardPool getFilteredPool(Predicate<PaperCard> predicate){
CardPool filteredPool = new CardPool();
for(PaperCard pc : this.items.keySet()){
if(predicate.apply(pc)) filteredPool.add(pc);
return filteredPool;
......@@ -78,7 +78,8 @@ public class GameRules {
public boolean hasCommander() {
return appliedVariants.contains(GameType.Commander) || appliedVariants.contains(GameType.TinyLeaders)
return appliedVariants.contains(GameType.Commander)
|| appliedVariants.contains(GameType.TinyLeaders)
|| appliedVariants.contains(GameType.Brawl);
......@@ -18,11 +18,20 @@
package forge.screens.deckeditor.controllers;
import forge.UiCommand;
import forge.card.CardRules;
import forge.card.CardRulesPredicates;
import forge.card.ColorSet;
import forge.card.mana.ManaCost;
import forge.deck.CardPool;
import forge.deck.Deck;
import forge.deck.DeckSection;
import forge.deck.generation.DeckGeneratorBase;
import forge.gui.GuiUtils;
import forge.gui.framework.DragCell;
import forge.gui.framework.FScreen;
......@@ -35,6 +44,7 @@ import forge.itemmanager.views.ItemTableColumn;
import forge.model.FModel;
import forge.screens.deckeditor.AddBasicLandsDialog;
import forge.screens.deckeditor.SEditorIO;
import forge.screens.deckeditor.views.VAllDecks;
......@@ -48,6 +58,7 @@ import forge.util.ItemPool;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.print.Paper;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
......@@ -103,6 +114,14 @@ public final class CEditorQuest extends CDeckEditor<Deck> {
//Add sub-format specific sections
case Default: break;
case Commander:
this.questData = questData0;
final CardManager catalogManager = new CardManager(cDetailPicture, false, true);
......@@ -158,6 +177,10 @@ public final class CEditorQuest extends CDeckEditor<Deck> {
protected CardLimit getCardLimit() {
if (FModel.getPreferences().getPrefBoolean(FPref.ENFORCE_DECK_LEGALITY)) {
//If this is a commander quest, only allow single copies of cards
if(FModel.getQuest().getDeckConstructionRules() == DeckConstructionRules.Commander){
return CardLimit.Singleton;
return CardLimit.Default;
return CardLimit.None; //if not enforcing deck legality, don't enforce default limit
......@@ -245,16 +268,98 @@ public final class CEditorQuest extends CDeckEditor<Deck> {
public void resetTables() {
this.sectionMode = DeckSection.Main;
final Deck deck = this.controller.getModel();
// show cards, makes this user friendly
* Provides the pool of cards the player has available to add to his or her deck. Also manages showing available cards
* to choose from for special deck construction rules, e.g.: Commander.
* @return CardPool of cards available to add to the player's deck.
private CardPool getRemainingCardPool(){
final CardPool cardpool = getInitialCatalog();
// remove bottom cards that are in the deck from the card pool
// remove sideboard cards from the catalog
// show cards, makes this user friendly
case Default: break;
case Commander:
//remove this deck's currently selected commander(s) from the catalog
//TODO: Only thin if deck conformance is being applied
if(getDeck().getOrCreate(DeckSection.Commander).toFlatList().size() > 0) {
Predicate<PaperCard> identityPredicate = new MatchCommanderColorIdentity(getDeckColorIdentity());
CardPool filteredPool = cardpool.getFilteredPool(identityPredicate);
return filteredPool;
return cardpool;
* Predicate that filters out based on a color identity provided upon instantiation. Used to filter the card
* list when a commander is chosen so the user can more easily see what cards are available for his or her deck
* and avoid making additions that are not legal.
public static class MatchCommanderColorIdentity implements Predicate<PaperCard> {
private final ColorSet allowedColor;
public MatchCommanderColorIdentity(ColorSet color) {
allowedColor = color;
public boolean apply(PaperCard subject) {
CardRules cr = subject.getRules();
ManaCost mc = cr.getManaCost();
return !mc.isPureGeneric() && allowedColor.containsAllColorsFrom(cr.getColorIdentity().getColor());
* Compiles the color identity of the loaded deck based on the commanders.
* @return A ColorSet containing the color identity of the currently loaded deck.
public ColorSet getDeckColorIdentity(){
List<PaperCard> commanders = getDeck().getOrCreate(DeckSection.Commander).toFlatList();
List<String> colors = new ArrayList<>();
//Return early if there are no current commanders
if(commanders.size() == 0) return ColorSet.fromNames(colors);
//For each commander,add each color of its color identity if not already added
for(PaperCard pc : commanders){
if(!colors.contains("w") && pc.getRules().getColorIdentity().hasWhite()) colors.add("w");
if(!colors.contains("u") && pc.getRules().getColorIdentity().hasBlue()) colors.add("u");
if(!colors.contains("b") && pc.getRules().getColorIdentity().hasBlack()) colors.add("b");
if(!colors.contains("r") && pc.getRules().getColorIdentity().hasRed()) colors.add("r");
if(!colors.contains("g") && pc.getRules().getColorIdentity().hasGreen()) colors.add("g");
return ColorSet.fromNames(colors);
Used to make the code more readable in game terms.
private Deck getDeck(){
return this.controller.getModel();
private ItemPool<PaperCard> getCommanderCardPool(){
Predicate<PaperCard> commanderPredicate = Predicates.compose(CardRulesPredicates.Presets.CAN_BE_COMMANDER, PaperCard.FN_GET_RULES);
return getRemainingCardPool().getFilteredPool(commanderPredicate);
......@@ -280,14 +385,30 @@ public final class CEditorQuest extends CDeckEditor<Deck> {
* Switch between the main deck and the sideboard editor.
* Switch between the main deck and the sideboard/Command Zone editor.
public void setEditorMode(DeckSection sectionMode) {
if (sectionMode == DeckSection.Sideboard) {
else {
//Fixes null pointer error on switching tabs while quest deck editor is open. TODO: Find source of bug possibly?
if(sectionMode == null) sectionMode = DeckSection.Main;
//Based on which section the editor is in, display the remaining card pool (or applicable card pool if in
//Commander) and the current section's cards
case Main :
case Sideboard :
case Commander :
this.sectionMode = sectionMode;
......@@ -10,6 +10,7 @@ import forge.model.FModel;
......@@ -340,9 +341,16 @@ public enum CSubmenuQuestData implements ICDoc {
//Apply the appropriate deck construction rules for this quest
DeckConstructionRules dcr = DeckConstructionRules.Default;
dcr = DeckConstructionRules.Commander;
final QuestController qc = FModel.getQuest();
qc.newGame(questName, difficulty, mode, fmtPrizes, view.isUnlockSetsAllowed(), dckStartPool, fmtStartPool, view.getStartingWorldName(), userPrefs);
qc.newGame(questName, difficulty, mode, fmtPrizes, view.isUnlockSetsAllowed(), dckStartPool, fmtStartPool, view.getStartingWorldName(), userPrefs, dcr);
// Save in preferences.
......@@ -62,6 +62,7 @@ public enum VSubmenuQuestData implements IVSubmenu<CSubmenuQuestData> {
private final FRadioButton radHard = new FRadioButton("Hard");
private final FRadioButton radExpert = new FRadioButton("Expert");
private final FCheckBox boxFantasy = new FCheckBox("Fantasy Mode");
private final FCheckBox boxCommander = new FCheckBox("Commander Subformat");
private final FLabel lblStartingWorld = new FLabel.Builder().text("Starting world:").build();
private final FComboBoxWrapper<QuestWorld> cbxStartingWorld = new FComboBoxWrapper<>();
......@@ -274,9 +275,25 @@ public enum VSubmenuQuestData implements IVSubmenu<CSubmenuQuestData> {
// Fantasy box enabled by Default
// Fantasy box selected by Default
// Commander box unselected by Default
new ActionListener(){
public void actionPerformed(ActionEvent e){
if(!isCommander()) return; //do nothing if unselecting Commander Subformat
//Otherwise, set the starting world to Random Commander
cbxStartingWorld.setSelectedItem(FModel.getWorlds().get("Random Commander"));
......@@ -286,6 +303,7 @@ public enum VSubmenuQuestData implements IVSubmenu<CSubmenuQuestData> {
final JPanel pnlDifficultyMode = new JPanel(new MigLayout("insets 0, gap 1%, flowy"));
pnlDifficultyMode.add(difficultyPanel, "gapright 4%");
pnlDifficultyMode.add(boxFantasy, "h 25px!, gapbottom 15, gapright 4%");
pnlDifficultyMode.add(boxCommander, "h 25px!, gapbottom 15, gapright 4%");
pnlDifficultyMode.add(lblStartingWorld, "h 25px!, hidemode 3");
cbxStartingWorld.addTo(pnlDifficultyMode, "h 27px!, w 40%, pushx, gapbottom 7");
......@@ -487,6 +505,14 @@ public enum VSubmenuQuestData implements IVSubmenu<CSubmenuQuestData> {
return boxFantasy.isSelected();
* Auth. Imakuni
* @return True if the "Commander Subformat" check box is selected.
public boolean isCommander() {
return boxCommander.isSelected();
public boolean startWithCompleteSet() {
return boxCompleteSet.isSelected();
Name:Main world
Name:Random Standard
Name:Random Commander
Name:Amonkhet|Dir:Amonkhet|Sets:AKH, HOU
Name:Jamuraa|Dir:jamuraa|Sets:5ED, ARN, MIR, VIS, WTH|Banned:Chaos Orb; Falling Star
Name:Kamigawa|Dir:2004 Kamigawa|Sets:CHK, BOK, SOK
......@@ -276,9 +276,10 @@ public class QuestController {
public void newGame(final String name, final int difficulty, final QuestMode mode,
final GameFormat formatPrizes, final boolean allowSetUnlocks,
final Deck startingCards, final GameFormat formatStartingPool,
final String startingWorld, final StartingPoolPreferences userPrefs) {
final String startingWorld, final StartingPoolPreferences userPrefs,
DeckConstructionRules dcr) {
this.load(new QuestData(name, difficulty, mode, formatPrizes, allowSetUnlocks, startingWorld)); // pass awards and unlocks here
this.load(new QuestData(name, difficulty, mode, formatPrizes, allowSetUnlocks, startingWorld, dcr)); // pass awards and unlocks here
if (startingCards != null) {
......@@ -435,6 +436,12 @@ public class QuestController {
QuestWorld world = getWorld();
String path = ForgeConstants.DEFAULT_CHALLENGES_DIR;
//Use a variant specialized duel manager if this is a variant quest
case Default: break;
case Commander: this.duelManager = new QuestEventCommanderDuelManager(); return;
if (world != null) {
if (world.getName().equals(QuestWorld.STANDARDWORLDNAME)) {
......@@ -449,7 +456,6 @@ public class QuestController {
this.duelManager = new QuestEventDuelManager(new File(path));
public HashSet<StarRating> GetRating() {
......@@ -607,4 +613,6 @@ public class QuestController {
public void setCurrentDeck(String s) {
model.currentDeck = s;
public DeckConstructionRules getDeckConstructionRules(){return model.deckConstructionRules;}
......@@ -48,6 +48,7 @@ public abstract class QuestEvent implements IQuestEvent {
private String profile = "Default";
// Opponent name if different from the challenge name
private String opponentName = null;
private boolean isRandomMatch = false;
public static final Function<QuestEvent, String> FN_GET_NAME = new Function<QuestEvent, String>() {
......@@ -174,4 +175,7 @@ public abstract class QuestEvent implements IQuestEvent {
this.showDifficulty = showDifficulty;
public boolean getIsRandomMatch(){return isRandomMatch;}
public void setIsRandomMatch(boolean b){isRandomMatch = b;}
import forge.deck.DeckProxy;
* A QuestEventDuel with a CommanderDeckGenerator used exclusively within QuestEventCommanderDuelManager for the
* creation of randomly generated Commander decks in a Commander variant quest.
* Auth. Imakuni & Forge
public class QuestEventCommanderDuel extends QuestEventDuel{
* The CommanderDeckGenerator for this duel.
private DeckProxy deckProxy;
public DeckProxy getDeckProxy() {return deckProxy;}
public void setDeckProxy(DeckProxy dp) {deckProxy = dp;}
import forge.deck.*;
import forge.item.PaperCard;
import forge.model.FModel;
import forge.util.CollectionSuppliers;
import forge.util.MyRandom;
import forge.util.maps.EnumMapOfLists;
import forge.util.maps.MapOfLists;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
* Manages the creation of random Commander duels for a Commander variant quest. Random generation is handled via
* the CommanderDeckGenerator class.
* Auth. Forge & Imakuni#8015
public class QuestEventCommanderDuelManager implements QuestEventDuelManagerInterface {
* The list of all possible Commander variant duels.
private ArrayList<QuestEventDuel> commanderDuels = new ArrayList<>();
* Contains the expert deck lists for the commanders.
private List<DeckProxy> expertCommanderDecks;
* Immediately calls assembleDuels() to setup the commanderDuels variable.
public QuestEventCommanderDuelManager(){
* Assembles the list of all possible Commander duels via CommanderDeckGenerator. Should be done within constructor.
private void assembleDuels(){
//isCardGen = true seemed to make slightly more difficult decks based purely on experience with a very small sample size.
//Gotta work on this more, its making pretty average decks after further testing.
expertCommanderDecks = CommanderDeckGenerator.getCommanderDecks(DeckFormat.Commander, true, true);
List<DeckProxy> generatedDuels = CommanderDeckGenerator.getCommanderDecks(DeckFormat.Commander, true, false);
for(DeckProxy dp : generatedDuels){
QuestEventCommanderDuel duel = new QuestEventCommanderDuel();
duel.setDescription("Randomly generated " + dp.getName() + " commander deck.");
//Setting a blank deck avoids a null pointer exception. The deck is generated in generateDuels() to avoid long load times.
duel.setEventDeck(new Deck());
* Retrieve list of all possible Commander duels.
* @return ArrayList containing all possible Commander duels.
public Iterable<QuestEventDuel> getAllDuels() {
return commanderDuels;
* Retrieve list of all possible Commander duels.
* @param difficulty Currently unused
* @return ArrayList containing all possible Commander duels.
public Iterable<QuestEventDuel> getDuels(QuestEventDifficulty difficulty){
return commanderDuels;
* Composes an ArrayList containing 4 QuestEventDuels composed with Commander variant decks. One duel will have its
* title replaced as Random.
* @return ArrayList of QuestEventDuels containing 4 duels.
public List<QuestEventDuel> generateDuels(){
final List<QuestEventDuel> duelOpponents = new ArrayList<>();
//While there are less than 4 duels chosen
while(duelOpponents.size() < 4){
//Get a random duel from the possible duels list
QuestEventCommanderDuel duel = (QuestEventCommanderDuel)commanderDuels.get(((int) (commanderDuels.size() * MyRandom.getRandom().nextDouble())));
//If the chosen duels list already contains this duel, get a different duel to prevent duplicate duels
if(duelOpponents.contains(duel)) continue;
//Add the randomly chosen duel to the duel list
//Here the actual deck for this commander is generated by calling .getDeck() on the saved DeckProxy
//Modify deck for difficulty
//Modify the stats of the final duel to hide the opponent, creating a "random" duel.
//We make a copy of the final duel and overwrite it in the duelOpponents to avoid changing the variables in
//the original duel, which gets reused.
QuestEventCommanderDuel duel = (QuestEventCommanderDuel)duelOpponents.get(duelOpponents.size() - 1);
QuestEventCommanderDuel randomDuel = new QuestEventCommanderDuel();
randomDuel.setTitle("Random Opponent");
randomDuel.setDescription("Fight a random generated commander opponent.");
//Replace the final duel with this newly modified "random" duel
duelOpponents.set(duelOpponents.size()-1, randomDuel);
return duelOpponents;
* Retrieves the expert level deck generation of a deck with the same commander as the provided DeckProxy.
* @param dp The easy generation commander deck
* @return The same commander's expert generation DeckProxy
private Deck getExpertGenDeck(DeckProxy dp){
for(QuestEventDuel qed : commanderDuels){
QuestEventCommanderDuel cmdQED = (QuestEventCommanderDuel)qed;
return cmdQED.getDeckProxy().getDeck();
return null;
* Modifies a given duel by replacing a percentage of the deck with random cards from the more difficult generated version
* of the same commander's deck. Medium replaces 30%, Hard replaces 60%, Expert replaces 100%.
* @param duel The QuestEventCommanderDuel to modify
private void modifyDuelForDifficulty(QuestEventCommanderDuel duel){
final QuestPreferences questPreferences = FModel.getQuestPreferences();
final int index = FModel.getQuest().getAchievements().getDifficulty();
final int numberOfWins = FModel.getQuest().getAchievements().getWin();
Deck expertDeck = getExpertGenDeck(duel.getDeckProxy());
int difficultyReplacementPercent = 0;
//Note: The code is ordered to make the least number of comparisons I could think of at the time for speed reasons.
//In reality, it shouldn't really make much difference, but why not?
if (numberOfWins >= questPreferences.getPrefInt(QuestPreferences.DifficultyPrefs.WINS_EXPERTAI, index)) {
//At expert, the deck is replaced with the entire expert deck, and we can return immediately
if (numberOfWins >= questPreferences.getPrefInt(QuestPreferences.DifficultyPrefs.WINS_MEDIUMAI, index)) {
difficultyReplacementPercent += 30;
} else return; //return early here since it would be an easy opponent with no changes
if (numberOfWins >= questPreferences.getPrefInt(QuestPreferences.DifficultyPrefs.WINS_HARDAI, index)) {
difficultyReplacementPercent += 30;
CardPool easyMain = duel.getEventDeck().getMain();
CardPool expertMain = expertDeck.getMain();
List<PaperCard> easyList = easyMain.toFlatList();
List<PaperCard> expertList = expertMain.toFlatList();
//Replace cards in the easy deck with cards from the expert deck up to the difficulty replacement percent
for(int i = 0; i < difficultyReplacementPercent; i++){
if(!easyMain.contains(expertList.get(i))) { //ensure that the card being copied over isn't already in the deck