- Reintegrate merge: AIPersonalities --> trunk.

- The primary API for AI personalities is now in place. AI properties are implemented in forge.game.ai.AiProps and are retrieved via a call to one of AiController.getProperty, AiController.getIntProperty, or AiController.getBooleanProperty depending on the type of property (AI controller can be retrieved via a getAi() call on the AI player object). Each property can be assigned on a per-profile basis in res/ai/*.ai profile files. A profile can be selected in the game properties. For an example implementation of a property, see AI_MULLIGAN_THRESHOLD implemented in AiProps and retrieved in ComputerUtil.
- Currently there is one property only, but more are on the way. Everyone is also free to expand as necessary. Specific AI profiles for specific quest opponents will be implemented at a later date.
This commit is contained in:
Agetian
2013-03-19 02:40:58 +00:00
parent 5ab6fd14a8
commit 94c36b571a
13 changed files with 350 additions and 5 deletions

3
.gitattributes vendored
View File

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

1
res/ai/Default.ai Normal file
View File

@@ -0,0 +1 @@
AI_MULLIGAN_THRESHOLD=5

View File

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

View File

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

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<String, Map<AiProps, String>> loadedProfiles = new HashMap<String, Map<AiProps, String>>();
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<String> 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<AiProps, String> loadProfile(final String profileName) {
Map<AiProps, String> profileMap = new HashMap<AiProps, String>();
List<String> 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<String> - an array of strings containing all
* available profiles.
*/
public static ArrayList<String> getAvailableProfiles()
{
final ArrayList<String> availableProfiles = new ArrayList<String>();
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<String> - an array list of strings containing all
* available profiles including special random profile tags.
*/
public static ArrayList<String> getProfilesDisplayList() {
final ArrayList<String> availableProfiles = new ArrayList<String>();
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<String> 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)));
}
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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 &emsp; {@link java.lang.String} */
AiProps(final String s0) {
this.strDefaultVal = s0;
}
/** @return {@link java.lang.String} */
public String getDefault() {
return strDefaultVal;
}
}

View File

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

View File

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

View File

@@ -164,7 +164,6 @@ public abstract class Player extends GameEntity implements Comparable<Player> {
ZoneType.Library, ZoneType.Graveyard, ZoneType.Hand, ZoneType.Exile, ZoneType.Command, ZoneType.Ante,
ZoneType.Sideboard));
protected final LobbyPlayer lobbyPlayer;
protected final GameState game;

View File

@@ -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<String> 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()
*/

View File

@@ -72,6 +72,15 @@ public enum VSubmenuPreferences implements IVSubmenu<CSubmenuPreferences> {
.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<CSubmenuPreferences> {
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<CSubmenuPreferences> {
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;

View File

@@ -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<ForgePreferences.FPref> {
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"),