Merge branch 'master' into 'master'

Commander quest mode

See merge request core-developers/forge!1053
This commit is contained in:
Michael Kamensky
2018-11-10 16:18:16 +00:00
20 changed files with 565 additions and 37 deletions

View File

@@ -1,5 +1,6 @@
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

View File

@@ -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) {
this.myCards.addDeck(startingCards);
@@ -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
switch(FModel.getQuest().getDeckConstructionRules()){
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;}
}

View File

@@ -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;}
}

View File

@@ -0,0 +1,19 @@
package forge.quest;
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;}
}

View File

@@ -0,0 +1,202 @@
package forge.quest;
import forge.deck.*;
import forge.item.PaperCard;
import forge.model.FModel;
import forge.quest.data.QuestPreferences;
import forge.util.MyRandom;
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(){
assembleDuels();
}
/**
* 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.");
duel.setName(dp.getName());
duel.setTitle(dp.getName());
duel.setOpponentName(dp.getName());
duel.setDifficulty(QuestEventDifficulty.EASY);
duel.setDeckProxy(dp);
//Setting a blank deck avoids a null pointer exception. The deck is generated in generateDuels() to avoid long load times.
duel.setEventDeck(new Deck());
commanderDuels.add(duel);
}
}
/**
* 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
duelOpponents.add(duel);
//Here the actual deck for this commander is generated by calling .getDeck() on the saved DeckProxy
duel.setEventDeck(duel.getDeckProxy().getDeck());
//Modify deck for difficulty
modifyDuelForDifficulty(duel);
}
//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.setName(duel.getName());
randomDuel.setOpponentName(duel.getName());
randomDuel.setDeckProxy(duel.getDeckProxy());
randomDuel.setTitle("Random Opponent");
randomDuel.setShowDifficulty(false);
randomDuel.setDescription("Fight a random generated commander opponent.");
randomDuel.setIsRandomMatch(true);
randomDuel.setEventDeck(duel.getEventDeck());
//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;
if(cmdQED.getDeckProxy().getName().equals(dp.getName())){
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
duel.setEventDeck(expertDeck);
duel.setDifficulty(QuestEventDifficulty.EXPERT);
return;
}
if (numberOfWins >= questPreferences.getPrefInt(QuestPreferences.DifficultyPrefs.WINS_MEDIUMAI, index)) {
difficultyReplacementPercent += 30;
duel.setDifficulty(QuestEventDifficulty.MEDIUM);
} 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;
duel.setDifficulty(QuestEventDifficulty.HARD);
}
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
easyMain.remove(easyList.get(i));
easyMain.add(expertList.get(i));
}
else{
expertList.remove(expertList.get(i));
i--;
if(expertList.size() == 0) break; //break if there are no more cards to copy over
}
}
}
/**
* Randomizes the list of Commander Duels.
*/
public void randomizeOpponents(){
Collections.shuffle(commanderDuels);
}
}

View File

@@ -344,11 +344,24 @@ public class QuestSpellShop {
List<Entry<InventoryItem, Integer>> cardsToRemove = new LinkedList<>();
for (Entry<InventoryItem, Integer> item : inventoryManager.getPool()) {
PaperCard card = (PaperCard)item.getKey();
int numToKeep = card.getRules().getType().isBasic() ?
FModel.getQuestPreferences().getPrefInt(QPref.PLAYSET_BASIC_LAND_SIZE) : FModel.getQuestPreferences().getPrefInt(QPref.PLAYSET_SIZE);
//Number of a particular card to keep
int numToKeep = 4;
if(card.getRules().getType().isBasic()){
numToKeep = FModel.getQuestPreferences().getPrefInt(QPref.PLAYSET_BASIC_LAND_SIZE);
} else{
//Choose card limit restrictions based on deck construction rules, e.g.: Commander allows only singletons
switch(FModel.getQuest().getDeckConstructionRules()){
case Default: numToKeep = FModel.getQuestPreferences().getPrefInt(QPref.PLAYSET_SIZE); break;
case Commander: numToKeep = 1;
}
}
//If this card has an exception to the card limit, e.g.: Relentless Rats, get the quest preference
if (DeckFormat.getLimitExceptions().contains(card.getName())) {
numToKeep = FModel.getQuestPreferences().getPrefInt(QPref.PLAYSET_ANY_NUMBER_SIZE);
}
if (numToKeep < item.getValue()) {
cardsToRemove.add(Pair.of(item.getKey(), item.getValue() - numToKeep));
}

View File

@@ -41,6 +41,7 @@ import forge.properties.ForgePreferences.FPref;
import forge.quest.bazaar.IQuestBazaarItem;
import forge.quest.bazaar.QuestItemType;
import forge.quest.bazaar.QuestPetController;
import forge.quest.data.DeckConstructionRules;
import forge.quest.data.QuestAchievements;
import forge.quest.data.QuestAssets;
import forge.util.gui.SGuiChoose;
@@ -51,6 +52,7 @@ import org.apache.commons.lang3.tuple.ImmutablePair;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeSet;
/**
* <p>
@@ -531,7 +533,17 @@ public class QuestUtil {
Integer lifeHuman = null;
boolean useBazaar = true;
Boolean forceAnte = null;
int lifeAI = 20;
//Generate a life modifier based on this quest's variant as held in the Quest Controller's DeckConstructionRules
int variantLifeModifier = 0;
switch(FModel.getQuest().getDeckConstructionRules()){
case Default: break;
case Commander: variantLifeModifier = 20; break;
}
int lifeAI = 20 + variantLifeModifier;
if (event instanceof QuestEventChallenge) {
final QuestEventChallenge qc = ((QuestEventChallenge) event);
lifeAI = qc.getAILife();
@@ -545,8 +557,9 @@ public class QuestUtil {
forceAnte = qc.isForceAnte();
}
final RegisteredPlayer humanStart = new RegisteredPlayer(getDeckForNewGame());
final RegisteredPlayer aiStart = new RegisteredPlayer(event.getEventDeck());
final RegisteredPlayer humanStart = getRegisteredPlayerByVariant(getDeckForNewGame());
final RegisteredPlayer aiStart = getRegisteredPlayerByVariant(event.getEventDeck());
if (lifeHuman != null) {
humanStart.setStartingLife(lifeHuman);
@@ -581,17 +594,39 @@ public class QuestUtil {
rules.setGamesPerMatch(qData.getMatchLength());
rules.setManaBurn(FModel.getPreferences().getPrefBoolean(FPref.UI_MANABURN));
rules.setCanCloneUseTargetsImage(FModel.getPreferences().getPrefBoolean(FPref.UI_CLONE_MODE_SOURCE));
TreeSet<GameType> variant = new TreeSet();
if(FModel.getQuest().getDeckConstructionRules() == DeckConstructionRules.Commander){
variant.add(GameType.Commander);
}
final HostedMatch hostedMatch = GuiBase.getInterface().hostMatch();
final IGuiGame gui = GuiBase.getInterface().getNewGuiGame();
gui.setPlayerAvatar(aiPlayer, event);
FThreads.invokeInEdtNowOrLater(new Runnable(){
@Override
public void run() {
hostedMatch.startMatch(rules, null, starter, ImmutableMap.of(humanStart, gui));
hostedMatch.startMatch(rules, variant, starter, ImmutableMap.of(humanStart, gui));
}
});
}
/**
* Uses the appropriate RegisteredPlayer command for generating a RegisteredPlayer based on this quest's variant as
* held by the QuestController's DeckConstructionRules.
* @param deck The deck to generate the RegisteredPlayer with
* @return A newly made RegisteredPlayer specific to the quest's variant
*/
private static RegisteredPlayer getRegisteredPlayerByVariant(Deck deck){
switch (FModel.getQuest().getDeckConstructionRules()) {
case Default:
return new RegisteredPlayer(deck);
case Commander:
return RegisteredPlayer.forCommander(deck);
}
return null;
}
private static Deck getDeckForNewGame() {
Deck deck = null;
if (event instanceof QuestEventChallenge) {
@@ -623,7 +658,7 @@ public class QuestUtil {
}
if (FModel.getPreferences().getPrefBoolean(FPref.ENFORCE_DECK_LEGALITY)) {
final String errorMessage = GameType.Quest.getDeckFormat().getDeckConformanceProblem(deck);
final String errorMessage = getDeckConformanceProblems(deck);
if (null != errorMessage) {
SOptionPane.showErrorDialog("Your deck " + errorMessage + " Please edit or choose a different deck.", "Invalid Deck");
return false;
@@ -633,6 +668,21 @@ public class QuestUtil {
return true;
}
public static String getDeckConformanceProblems(Deck deck){
String errorMessage = GameType.Quest.getDeckFormat().getDeckConformanceProblem(deck);;
if(errorMessage != null) return errorMessage; //return immediately if the deck does not conform to quest requirements
//Check for all applicable deck construction rules per this quests's saved DeckConstructionRules enum
switch(FModel.getQuest().getDeckConstructionRules()){
case Commander:
errorMessage = GameType.Commander.getDeckFormat().getDeckConformanceProblem(deck);
break;
}
return errorMessage;
}
/** Duplicate in DeckEditorQuestMenu and
* probably elsewhere...can streamline at some point
* (probably shouldn't be here).

View File

@@ -308,10 +308,16 @@ public final class QuestUtilCards {
* user preferences
*/
public void setupNewGameCardPool(final GameFormat formatStartingPool, final int idxDifficulty, final StartingPoolPreferences userPrefs) {
//Add additional cards to the starter card pool based on variant if applicable
double variantModifier = 0;
switch(FModel.getQuest().getDeckConstructionRules()){
case Default: break;
case Commander: variantModifier = 2; break;
}
final int nC = questPreferences.getPrefInt(DifficultyPrefs.STARTING_COMMONS, idxDifficulty);
final int nU = questPreferences.getPrefInt(DifficultyPrefs.STARTING_UNCOMMONS, idxDifficulty);
final int nR = questPreferences.getPrefInt(DifficultyPrefs.STARTING_RARES, idxDifficulty);
final int nC = (int)(questPreferences.getPrefInt(DifficultyPrefs.STARTING_COMMONS, idxDifficulty) * variantModifier);
final int nU = (int)(questPreferences.getPrefInt(DifficultyPrefs.STARTING_UNCOMMONS, idxDifficulty) * variantModifier);
final int nR = (int)(questPreferences.getPrefInt(DifficultyPrefs.STARTING_RARES, idxDifficulty) * variantModifier);
addAllCards(BoosterUtils.getQuestStarterDeck(formatStartingPool, nC, nU, nR, userPrefs));

View File

@@ -226,6 +226,11 @@ public class QuestWinLoseController {
sb.append(StringUtils.capitalize(qEvent.getDifficulty().getTitle()));
sb.append(" opponent: ").append(credBase).append(" credits.\n");
if(qEvent.getIsRandomMatch()){
sb.append("Random Opponent Bonus: " + credBase + " credit" + (credBase > 1 ? "s." : ".") + "\n");
credBase += credBase;
}
final int winMultiplier = Math.min(qData.getAchievements().getWin(), FModel.getQuestPreferences().getPrefInt(QPref.REWARDS_WINS_MULTIPLIER_MAX));
final int creditsForPreviousWins = (int) ((Double.parseDouble(FModel.getQuestPreferences()
.getPref(QPref.REWARDS_WINS_MULTIPLIER)) * winMultiplier));

View File

@@ -40,6 +40,7 @@ public class QuestWorld implements Comparable<QuestWorld>{
private final String dir;
private final GameFormatQuest format;
public static final String STANDARDWORLDNAME = "Random Standard";
public static final String RANDOMCOMMANDERWORLDNAME = "Random Commander";
private boolean isCustom;
@@ -129,7 +130,6 @@ public class QuestWorld implements Comparable<QuestWorld>{
/**
* TODO: Write javadoc for Constructor.
* @param file0
* @param keySelector0
*/
public Reader(String file0) {
super(file0, QuestWorld.FN_GET_NAME);
@@ -194,6 +194,12 @@ public class QuestWorld implements Comparable<QuestWorld>{
FModel.getFormats().getStandard().getBannedCardNames(),false);
}
if (useName.equalsIgnoreCase(QuestWorld.RANDOMCOMMANDERWORLDNAME)){
useFormat = new GameFormatQuest(QuestWorld.RANDOMCOMMANDERWORLDNAME,
FModel.getFormats().getFormat("Commander").getAllowedSetCodes(),
FModel.getFormats().getFormat("Commander").getBannedCardNames(),false);
}
// System.out.println("Creating quest world " + useName + " (index " + useIdx + ", dir: " + useDir);
// if (useFormat != null) { System.out.println("SETS: " + sets + "\nBANNED: " + bannedCards); }

View File

@@ -0,0 +1,17 @@
package forge.quest.data;
/**
* Used to clarify which subformat a quest is using e.g. Commander.
* Auth. Imakuni
*/
public enum DeckConstructionRules {
/**
* Typically has no effect on Quest gameplay.
*/
Default,
/**
* Commander ruleset. 99 card deck, no copies other than basic lands, commander(s) in Command zone
*/
Commander
}

View File

@@ -200,7 +200,14 @@ public class QuestAssets {
* @return the life
*/
public int getLife(final QuestMode mode) {
final int base = mode.equals(QuestMode.Fantasy) ? 15 : 20;
int base = mode.equals(QuestMode.Fantasy) ? 15 : 20;
//Modify life for the quest's sub-format, e.g.: Commander adds 20
switch(FModel.getQuest().getDeckConstructionRules()){
case Default: break;
case Commander: base += 20;
}
return (base + this.getItemLevel(QuestItemType.ELIXIR_OF_LIFE)) - this.getItemLevel(QuestItemType.POUND_FLESH);
}

View File

@@ -42,7 +42,7 @@ import java.util.Map;
*/
public final class QuestData {
/** Holds the latest version of the Quest Data. */
public static final int CURRENT_VERSION_NUMBER = 12;
public static final int CURRENT_VERSION_NUMBER = 13;
// This field places the version number into QD instance,
// but only when the object is created through the constructor
@@ -70,6 +70,11 @@ public final class QuestData {
public String currentDeck = "DEFAULT";
/**
* Holds the subformat for this quest. Defaults to DeckConstructionRules.Default.
*/
public DeckConstructionRules deckConstructionRules = DeckConstructionRules.Default;
public QuestData() { //needed for XML serialization
}
@@ -87,9 +92,11 @@ public final class QuestData {
* allow set unlocking during quest
* @param startingWorld
* starting world
* @param dcr
* deck construction rules e.g. Commander
*/
public QuestData(String name0, int diff, QuestMode mode0, GameFormat userFormat,
boolean allowSetUnlocks, final String startingWorld) {
boolean allowSetUnlocks, final String startingWorld, DeckConstructionRules dcr) {
this.name = name0;
if (userFormat != null) {
@@ -99,6 +106,7 @@ public final class QuestData {
this.achievements = new QuestAchievements(diff);
this.assets = new QuestAssets(format);
this.worldId = startingWorld;
this.deckConstructionRules = dcr;
}
/**

View File

@@ -223,10 +223,16 @@ public class QuestDataIO {
// Current Deck moved from preferences to quest data - it should not be global for all quests!!!
QuestDataIO.setFinalField(QuestData.class, "currentDeck", newData, FModel.getQuestPreferences().getPref(QPref.CURRENT_DECK));
}
if (saveVersion < 13) {
if(saveVersion < 13){
//Update for quest DeckConstructionRules
//Add a DeckConstructionRules set to Default.
QuestDataIO.setFinalField(QuestData.class, "deckConstructionRules", newData, DeckConstructionRules.Default);
}
if (saveVersion < 14) {
// Migrate DraftTournaments to use new Tournament class
}
final QuestAssets qS = newData.getAssets();
final QuestAchievements qA = newData.getAchievements();