diff --git a/.gitattributes b/.gitattributes index 923154272da..3f33ee2791e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,6 +10,7 @@ /LICENSE.txt svneol=native#text/plain /README.txt svneol=native#text/plain /pom.xml svneol=native#text/xml +res/ai/Default.ai -text res/blockdata/blocks.txt svneol=native#text/plain res/blockdata/boosters.txt -text res/blockdata/fantasyblocks.txt -text @@ -14132,6 +14133,8 @@ src/main/java/forge/game/ai/AiAttackController.java svneol=native#text/plain src/main/java/forge/game/ai/AiController.java svneol=native#text/plain src/main/java/forge/game/ai/AiInputBlock.java -text src/main/java/forge/game/ai/AiInputCommon.java svneol=native#text/plain +src/main/java/forge/game/ai/AiProfileUtil.java -text +src/main/java/forge/game/ai/AiProps.java -text src/main/java/forge/game/ai/ComputerUtil.java svneol=native#text/plain src/main/java/forge/game/ai/ComputerUtilBlock.java svneol=native#text/plain src/main/java/forge/game/ai/ComputerUtilCard.java -text diff --git a/res/ai/Default.ai b/res/ai/Default.ai new file mode 100644 index 00000000000..ff5239c5d8b --- /dev/null +++ b/res/ai/Default.ai @@ -0,0 +1 @@ +AI_MULLIGAN_THRESHOLD=5 diff --git a/src/main/java/forge/control/FControl.java b/src/main/java/forge/control/FControl.java index c4310a85786..fffeba57872 100644 --- a/src/main/java/forge/control/FControl.java +++ b/src/main/java/forge/control/FControl.java @@ -34,6 +34,7 @@ import javax.swing.WindowConstants; import forge.Singletons; import forge.control.KeyboardShortcuts.Shortcut; +import forge.game.ai.AiProfileUtil; import forge.game.player.Player; import forge.gui.SOverlayUtils; import forge.gui.deckeditor.CDeckEditorUI; @@ -162,6 +163,9 @@ public enum FControl { Singletons.getModel().getQuest().load(QuestDataIO.loadData(data)); } + // Preload AI profiles + AiProfileUtil.loadAllProfiles(); + // Handles resizing in null layouts of layers in JLayeredPane. Singletons.getView().getFrame().addComponentListener(new ComponentAdapter() { @Override diff --git a/src/main/java/forge/game/MatchController.java b/src/main/java/forge/game/MatchController.java index e0dd57a45dc..73967a75d90 100644 --- a/src/main/java/forge/game/MatchController.java +++ b/src/main/java/forge/game/MatchController.java @@ -13,6 +13,7 @@ import forge.control.FControl; import forge.control.input.InputControl; import forge.deck.Deck; import forge.error.BugReporter; +import forge.game.ai.AiProfileUtil; import forge.game.event.DuelOutcomeEvent; import forge.game.player.LobbyPlayer; import forge.game.player.Player; @@ -137,6 +138,26 @@ public class MatchController { startConditions.put(p, players.get(p.getLobbyPlayer())); } + // Set the current AI profile. + for (Player p : currentGame.getPlayers()) { + if (p.getType() == PlayerType.COMPUTER) { + if (Singletons.getModel().getPreferences().getPref(FPref.UI_CURRENT_AI_PROFILE).equals(AiProfileUtil.AI_PROFILE_RANDOM_DUEL)) { + String randomProfile = AiProfileUtil.getRandomProfile(); + p.getLobbyPlayer().setAiProfile(randomProfile); + } else if (Singletons.getModel().getPreferences().getPref(FPref.UI_CURRENT_AI_PROFILE).equals(AiProfileUtil.AI_PROFILE_RANDOM_MATCH)) { + if (this.getPlayedGames().isEmpty()) { + String randomProfile = AiProfileUtil.getRandomProfile(); + p.getLobbyPlayer().setAiProfile(randomProfile); + } + } else { + // TODO: implement specific AI profiles for quest mode. + String profile = Singletons.getModel().getPreferences().getPref(FPref.UI_CURRENT_AI_PROFILE); + p.getLobbyPlayer().setAiProfile(profile); + } + System.out.println(String.format("AI profile %s was chosen for the lobby player %s.", p.getLobbyPlayer().getAiProfile(), p.getLobbyPlayer().getName())); + } + } + try { Player localHuman = Aggregates.firstFieldEquals(currentGame.getPlayers(), Player.Accessors.FN_GET_TYPE, PlayerType.HUMAN); FControl.SINGLETON_INSTANCE.setPlayer(localHuman); diff --git a/src/main/java/forge/game/ai/AiController.java b/src/main/java/forge/game/ai/AiController.java index 3db947596de..dbba372c178 100644 --- a/src/main/java/forge/game/ai/AiController.java +++ b/src/main/java/forge/game/ai/AiController.java @@ -719,5 +719,17 @@ public class AiController { } return null; } + + public String getProperty(AiProps propName) { + return AiProfileUtil.getAIProp(getPlayer().getLobbyPlayer(), propName); + } + + public int getIntProperty(AiProps propName) { + return Integer.parseInt(AiProfileUtil.getAIProp(getPlayer().getLobbyPlayer(), propName)); + } + + public boolean getBooleanProperty(AiProps propName) { + return Boolean.parseBoolean(AiProfileUtil.getAIProp(getPlayer().getLobbyPlayer(), propName)); + } } diff --git a/src/main/java/forge/game/ai/AiProfileUtil.java b/src/main/java/forge/game/ai/AiProfileUtil.java new file mode 100644 index 00000000000..3db912f0a0a --- /dev/null +++ b/src/main/java/forge/game/ai/AiProfileUtil.java @@ -0,0 +1,184 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2013 Forge Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package forge.game.ai; + +import forge.Singletons; +import forge.game.player.LobbyPlayer; +import forge.util.Aggregates; +import forge.util.FileUtil; + +import java.io.File; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.ArrayList; + +/** + * Holds default AI personality profile values in an enum. + * Loads profile from the given text file when setProfile is called. + * If a requested value is not loaded from a profile, default is returned. + * + * @author Forge + * @version $Id: AIProfile.java 20169 2013-03-08 08:24:17Z Agetian $ + */ +public class AiProfileUtil { + private static Map> loadedProfiles = new HashMap>(); + + private static final String AI_PROFILE_DIR = "res/ai"; + private static final String AI_PROFILE_EXT = ".ai"; + + public static final String AI_PROFILE_RANDOM_MATCH = "* Random (Match) *"; + public static final String AI_PROFILE_RANDOM_DUEL = "* Random (Duel) *"; + + /** Builds an AI profile file name with full relative + * path based on the profile name. + * @param profileName the name of the profile. + * @return the full relative path and file name for the given profile. + */ + private static String buildFileName(final String profileName) { + return String.format("%s/%s%s", AI_PROFILE_DIR, profileName, AI_PROFILE_EXT); + } + + /** + * Load all profiles + */ + public static final void loadAllProfiles() { + loadedProfiles.clear(); + ArrayList availableProfiles = getAvailableProfiles(); + for (String profile : availableProfiles) { + loadedProfiles.put(profile, loadProfile(profile)); + } + } + + /** + * Load a single profile. + * @param profileName a profile to load. + */ + private static final Map loadProfile(final String profileName) { + Map profileMap = new HashMap(); + + List lines = FileUtil.readFile(buildFileName(profileName)); + for (String line : lines) { + + if (line.startsWith("#") || (line.length() == 0)) { + continue; + } + + final String[] split = line.split("="); + + if (split.length == 2) { + profileMap.put(AiProps.valueOf(split[0]), split[1]); + } else if (split.length == 1 && line.endsWith("=")) { + profileMap.put(AiProps.valueOf(split[0]), ""); + } + } + + return profileMap; + } + + /** + * Returns an AI property value for the current profile. + * + * @param fp0 an AI property. + * @return String + */ + public static String getAIProp(final LobbyPlayer p, final AiProps fp0) { + String val = null; + String profile = p.getAiProfile(); + + if (loadedProfiles.get(profile) != null) { + val = loadedProfiles.get(profile).get(fp0); + } + if (val == null) { val = fp0.getDefault(); } + + return val; + } + + /** + * Returns an array of strings containing all available profiles. + * @return ArrayList - an array of strings containing all + * available profiles. + */ + public static ArrayList getAvailableProfiles() + { + final ArrayList availableProfiles = new ArrayList(); + + final File dir = new File(AI_PROFILE_DIR); + final String[] children = dir.list(); + if (children == null) { + System.err.println("AIProfile > can't find AI profile directory!"); + } else { + for (int i = 0; i < children.length; i++) { + if (children[i].endsWith(AI_PROFILE_EXT)) { + availableProfiles.add(children[i].substring(0, children[i].length() - AI_PROFILE_EXT.length())); + } + } + } + + return availableProfiles; + } + + /** + * Returns an array of strings containing all available profiles including + * the special "Random" profiles. + * @return ArrayList - an array list of strings containing all + * available profiles including special random profile tags. + */ + public static ArrayList getProfilesDisplayList() { + final ArrayList availableProfiles = new ArrayList(); + availableProfiles.add(AI_PROFILE_RANDOM_MATCH); + availableProfiles.add(AI_PROFILE_RANDOM_DUEL); + availableProfiles.addAll(getAvailableProfiles()); + + return availableProfiles; + } + + /** + * Returns a random personality from the currently available ones. + * @return String - a string containing a random profile from all the + * currently available ones. + */ + public static String getRandomProfile() { + return Aggregates.random(getAvailableProfiles()); + } + + /** + * Simple class test facility for AiProfileUtil. + */ + public static void selfTest() { + final LobbyPlayer activePlayer = Singletons.getControl().getPlayer().getLobbyPlayer(); + System.out.println(String.format("Current profile = %s", activePlayer.getAiProfile())); + ArrayList profiles = getAvailableProfiles(); + System.out.println(String.format("Available profiles: %s", profiles)); + if (profiles.size() > 0) { + System.out.println(String.format("Loading all profiles...")); + loadAllProfiles(); + System.out.println(String.format("Setting profile %s...", profiles.get(0))); + activePlayer.setAiProfile(profiles.get(0)); + for (AiProps property : AiProps.values()) { + System.out.println(String.format("%s = %s", property, getAIProp(activePlayer, property))); + } + String randomProfile = getRandomProfile(); + System.out.println(String.format("Loading random profile %s...", randomProfile)); + activePlayer.setAiProfile(randomProfile); + for (AiProps property : AiProps.values()) { + System.out.println(String.format("%s = %s", property, getAIProp(activePlayer, property))); + } + } + } +} diff --git a/src/main/java/forge/game/ai/AiProps.java b/src/main/java/forge/game/ai/AiProps.java new file mode 100644 index 00000000000..3484e5b70a4 --- /dev/null +++ b/src/main/java/forge/game/ai/AiProps.java @@ -0,0 +1,41 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2013 Forge Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package forge.game.ai; + +/** + * AI personality profile settings identifiers, and their default values. + * When this class is instantiated, these enum values are used + * in a map that is populated with the current AI profile settings + * from the text file. + */ +public enum AiProps { /** */ + AI_MULLIGAN_THRESHOLD ("5"); /** */ + + private final String strDefaultVal; + + /** @param s0   {@link java.lang.String} */ + AiProps(final String s0) { + this.strDefaultVal = s0; + } + + /** @return {@link java.lang.String} */ + public String getDefault() { + return strDefaultVal; + } +} + diff --git a/src/main/java/forge/game/ai/ComputerUtil.java b/src/main/java/forge/game/ai/ComputerUtil.java index ae723cd7071..fa0f93d9635 100644 --- a/src/main/java/forge/game/ai/ComputerUtil.java +++ b/src/main/java/forge/game/ai/ComputerUtil.java @@ -1202,11 +1202,9 @@ public class ComputerUtil { // Computer mulligans if there are no cards with converted mana cost of // 0 in its hand public static boolean wantMulligan(AIPlayer ai) { - final int AI_MULLIGAN_THRESHOLD = 5; - final List handList = ai.getCardsIn(ZoneType.Hand); final boolean hasLittleCmc0Cards = CardLists.getValidCards(handList, "Card.cmcEQ0", ai, null).size() < 2; - return (handList.size() > AI_MULLIGAN_THRESHOLD) && hasLittleCmc0Cards; + return (handList.size() > ai.getAi().getIntProperty(AiProps.AI_MULLIGAN_THRESHOLD)) && hasLittleCmc0Cards; } diff --git a/src/main/java/forge/game/player/LobbyPlayer.java b/src/main/java/forge/game/player/LobbyPlayer.java index 2efb0d9281d..28adde731b1 100644 --- a/src/main/java/forge/game/player/LobbyPlayer.java +++ b/src/main/java/forge/game/player/LobbyPlayer.java @@ -1,5 +1,8 @@ package forge.game.player; +import forge.game.ai.AiProfileUtil; +import forge.game.ai.AiProps; + /** * This means a player's part unchanged for all games. * @@ -17,6 +20,9 @@ public class LobbyPlayer implements IHasIcon { protected String imageKey; private int avatarIndex = -1; + /** The AI profile. */ + private String aiProfile = ""; + public LobbyPlayer(PlayerType type, String name) { this.type = type; @@ -75,4 +81,12 @@ public class LobbyPlayer implements IHasIcon { public void setAvatarIndex(int avatarIndex) { this.avatarIndex = avatarIndex; } + + public void setAiProfile(String profileName) { + aiProfile = profileName; + } + + public String getAiProfile() { + return aiProfile; + } } diff --git a/src/main/java/forge/game/player/Player.java b/src/main/java/forge/game/player/Player.java index 5da39b7bc01..a595e1c7445 100644 --- a/src/main/java/forge/game/player/Player.java +++ b/src/main/java/forge/game/player/Player.java @@ -164,7 +164,6 @@ public abstract class Player extends GameEntity implements Comparable { ZoneType.Library, ZoneType.Graveyard, ZoneType.Hand, ZoneType.Exile, ZoneType.Command, ZoneType.Ante, ZoneType.Sideboard)); - protected final LobbyPlayer lobbyPlayer; protected final GameState game; diff --git a/src/main/java/forge/gui/home/settings/CSubmenuPreferences.java b/src/main/java/forge/gui/home/settings/CSubmenuPreferences.java index 449b438fef1..988ee96cc89 100644 --- a/src/main/java/forge/gui/home/settings/CSubmenuPreferences.java +++ b/src/main/java/forge/gui/home/settings/CSubmenuPreferences.java @@ -13,10 +13,12 @@ import forge.Command; import forge.Constant.Preferences; import forge.Singletons; import forge.control.RestartUtil; +import forge.game.ai.AiProfileUtil; import forge.gui.framework.ICDoc; import forge.gui.toolbox.FSkin; import forge.properties.ForgePreferences; import forge.properties.ForgePreferences.FPref; +import java.util.ArrayList; /** * Controls the preferences submenu in the home UI. @@ -44,6 +46,13 @@ public enum CSubmenuPreferences implements ICDoc { } }); + view.getLstChooseAIProfile().addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(final MouseEvent e) { + updateAIProfile(); + } + }); + view.getCbAnte().addItemListener(new ItemListener() { @Override public void itemStateChanged(final ItemEvent arg0) { @@ -187,6 +196,7 @@ public enum CSubmenuPreferences implements ICDoc { final VSubmenuPreferences view = VSubmenuPreferences.SINGLETON_INSTANCE; final ForgePreferences prefs = Singletons.getModel().getPreferences(); updateSkinNames(); + updateAIProfiles(); view.getCbRemoveSmall().setSelected(prefs.getPrefBoolean(FPref.DECKGEN_NOSMALL)); view.getCbSingletons().setSelected(prefs.getPrefBoolean(FPref.DECKGEN_SINGLETONS)); @@ -226,6 +236,21 @@ public enum CSubmenuPreferences implements ICDoc { view.getLstChooseSkin().ensureIndexIsVisible(view.getLstChooseSkin().getSelectedIndex()); } + private void updateAIProfiles() { + final VSubmenuPreferences view = VSubmenuPreferences.SINGLETON_INSTANCE; + final ArrayList profileNames = AiProfileUtil.getProfilesDisplayList(); + final String currentName = Singletons.getModel().getPreferences().getPref(FPref.UI_CURRENT_AI_PROFILE); + int currentIndex = 0; + + for (int i = 0; i < profileNames.size(); i++) { + if (currentName.equalsIgnoreCase(profileNames.get(i))) { currentIndex = i; } + } + + view.getLstChooseAIProfile().setListData(profileNames.toArray()); + view.getLstChooseAIProfile().setSelectedIndex(currentIndex); + view.getLstChooseAIProfile().ensureIndexIsVisible(view.getLstChooseAIProfile().getSelectedIndex()); + } + @SuppressWarnings("serial") private void updateSkin() { final VSubmenuPreferences view = VSubmenuPreferences.SINGLETON_INSTANCE; @@ -243,6 +268,16 @@ public enum CSubmenuPreferences implements ICDoc { prefs.save(); } + @SuppressWarnings("serial") + private void updateAIProfile() { + final VSubmenuPreferences view = VSubmenuPreferences.SINGLETON_INSTANCE; + final String name = view.getLstChooseAIProfile().getSelectedValue().toString(); + final ForgePreferences prefs = Singletons.getModel().getPreferences(); + if (name.equals(prefs.getPref(FPref.UI_CURRENT_AI_PROFILE))) { return; } + + prefs.setPref(FPref.UI_CURRENT_AI_PROFILE, name); + prefs.save(); + } /* (non-Javadoc) * @see forge.gui.framework.ICDoc#getCommandOnSelect() */ diff --git a/src/main/java/forge/gui/home/settings/VSubmenuPreferences.java b/src/main/java/forge/gui/home/settings/VSubmenuPreferences.java index 925ab3c3348..d8306dd4995 100644 --- a/src/main/java/forge/gui/home/settings/VSubmenuPreferences.java +++ b/src/main/java/forge/gui/home/settings/VSubmenuPreferences.java @@ -64,7 +64,7 @@ public enum VSubmenuPreferences implements IVSubmenu { .hoverable(true).text("Reset to defaults").build(); private final FLabel lblTitleSkin = new FLabel.Builder() - .text("Choose Skin").fontStyle(Font.BOLD).fontSize(14).build(); + .text("Choose Skin").fontStyle(Font.BOLD).fontSize(14).build(); private final JList lstChooseSkin = new FList(); private final FLabel lblChooseSkin = new FLabel.Builder().fontSize(12).fontStyle(Font.ITALIC) @@ -72,6 +72,15 @@ public enum VSubmenuPreferences implements IVSubmenu { .fontAlign(SwingConstants.LEFT).build(); private final JScrollPane scrChooseSkin = new FScrollPane(lstChooseSkin); + private final FLabel lblTitleAIProfile = new FLabel.Builder() + .text("Choose AI Personality").fontStyle(Font.BOLD).fontSize(14).build(); + + private final JList lstChooseAIProfile = new FList(); + private final FLabel lblChooseAIProfile = new FLabel.Builder().fontSize(12).fontStyle(Font.ITALIC) + .text("AI Opponent Personality.") + .fontAlign(SwingConstants.LEFT).build(); + private final JScrollPane scrChooseAIProfile = new FScrollPane(lstChooseAIProfile); + private final JCheckBox cbRemoveSmall = new OptionsCheckBox("Remove Small Creatures"); private final JCheckBox cbSingletons = new OptionsCheckBox("Singleton Mode"); private final JCheckBox cbRemoveArtifacts = new OptionsCheckBox("Remove Artifacts"); @@ -134,6 +143,13 @@ public enum VSubmenuPreferences implements IVSubmenu { pnlPrefs.add(cbEnforceDeckLegality, regularConstraints); pnlPrefs.add(new NoteLabel("Enforces deck legality relevant to each environment (minimum deck sizes, max card count etc)"), regularConstraints); + // AI Personality Profile Options + pnlPrefs.add(new SectionLabel("AI Options"), sectionConstraints); + + pnlPrefs.add(lblTitleAIProfile, regularConstraints); + pnlPrefs.add(lblChooseAIProfile, regularConstraints); + pnlPrefs.add(scrChooseAIProfile, "h 200px!, w 200px!, gap 10% 0 0 2%, wrap"); + // Graphic Options pnlPrefs.add(new SectionLabel("Graphic Options"), sectionConstraints); @@ -355,6 +371,21 @@ public enum VSubmenuPreferences implements IVSubmenu { return scrChooseSkin; } + /** @return {@link javax.swing.JList} */ + public final JList getLstChooseAIProfile() { + return lstChooseAIProfile; + } + + /** @return {@link forge.gui.toolbox.FLabel} */ + public final FLabel getLblChooseAIProfile() { + return lblChooseAIProfile; + } + + /** @return {@link javax.swing.JScrollPane} */ + public final JScrollPane getScrChooseAIProfile() { + return scrChooseAIProfile; + } + /** @return {@link javax.swing.JCheckBox} */ public final JCheckBox getCbRemoveSmall() { return cbRemoveSmall; diff --git a/src/main/java/forge/properties/ForgePreferences.java b/src/main/java/forge/properties/ForgePreferences.java index 0e5a4e4bafe..8a951c5f44d 100644 --- a/src/main/java/forge/properties/ForgePreferences.java +++ b/src/main/java/forge/properties/ForgePreferences.java @@ -21,6 +21,7 @@ import java.util.List; import forge.Constant; import forge.Constant.Preferences; +import forge.game.ai.AiProfileUtil; import forge.gui.home.EMenuItem; import forge.gui.match.VMatchUI; import forge.gui.match.nonsingleton.VField; @@ -50,6 +51,7 @@ public class ForgePreferences extends PreferencesStore { UI_TARGETING_OVERLAY ("false"), UI_ENABLE_SOUNDS ("true"), UI_RANDOM_CARD_ART ("false"), + UI_CURRENT_AI_PROFILE (AiProfileUtil.AI_PROFILE_RANDOM_MATCH), /** */ SUBMENU_CURRENTMENU (EMenuItem.CONSTRUCTED.toString()), SUBMENU_SANCTIONED ("false"),