diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/attachments/RulesAttachment.java b/game-app/game-core/src/main/java/games/strategy/triplea/attachments/RulesAttachment.java index 1908d9db9cc..88acaf29b68 100644 --- a/game-app/game-core/src/main/java/games/strategy/triplea/attachments/RulesAttachment.java +++ b/game-app/game-core/src/main/java/games/strategy/triplea/attachments/RulesAttachment.java @@ -3,6 +3,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import com.google.common.annotations.VisibleForTesting; import games.strategy.engine.data.Attachable; import games.strategy.engine.data.BattleRecordsList; import games.strategy.engine.data.GameData; @@ -13,6 +14,7 @@ import games.strategy.engine.data.MutableProperty; import games.strategy.engine.data.RelationshipTracker.Relationship; import games.strategy.engine.data.RelationshipType; +import games.strategy.engine.data.Resource; import games.strategy.engine.data.TechnologyFrontier; import games.strategy.engine.data.Territory; import games.strategy.engine.data.Unit; @@ -55,9 +57,13 @@ public class RulesAttachment extends AbstractPlayerRulesAttachment { private int techCount = -1; // condition for having specific relationships private @Nullable List relationship = null; + // condition for checking ai player + private boolean checkAIPlayer = false; // condition for being at war private @Nullable Set atWarPlayers = null; private int atWarCount = -1; + // condition for checking resources + private @Nullable String[] haveResources = null; // condition for having destroyed at least X enemy non-neutral TUV (total unit value) [according // to // the prices the defender pays for the units] @@ -145,6 +151,50 @@ public static Set getNationalObjectives(final GamePlayer player return natObjs; } + @VisibleForTesting + public void setHaveResources(final String value) throws GameParseException { + final String[] s = splitOnColon(value); + if (s.length <= 1) { + throw new GameParseException( + "haveResources must have at least 2 fields. Format value=resource1 count=number, or " + + "value=resource1:resource2:resource3 count=number" + + thisErrorMsg()); + } + if ((s.length <= 2) && (s[1].equalsIgnoreCase("sum") || s[1].equalsIgnoreCase("add"))) { + throw new GameParseException( + "haveResources must have at least 3 fields when used with 'Sum' or 'Add'. Format value=Sum:resource1 " + + "count=number, or value=Sum:resource1:resource2:resource3 count=number" + + thisErrorMsg()); + } + final int n = getInt(s[0]); + if (n < 1) { + throw new GameParseException("haveResources must be a positive integer" + thisErrorMsg()); + } + for (int i = 1;i < s.length; i++) { + if (s[i].equalsIgnoreCase("sum") || s[i].equalsIgnoreCase("add")) { + i++; + } + // validate that this resource exists in the xml + final Resource r = getData().getResourceList().getResource(s[i]); + if (r == null) { + throw new GameParseException("No resource called: " + s[i] + thisErrorMsg()); + } + } + haveResources = s; + } + + private void setHaveResources(final String[] value) { + haveResources = value; + } + + public String[] getHaveResources() { + return haveResources; + } + + private void resetHaveResources() { + haveResources = null; + } + private void setDestroyedTuv(final String value) throws GameParseException { final String[] s = splitOnColon(value); if (s.length != 2) { @@ -489,6 +539,22 @@ private void resetUnitPresence() { unitPresence = null; } + private void setCheckAIPlayer(final String s) { + checkAIPlayer = getBool(s); + } + + private void setCheckAIPlayer(final Boolean s) { + checkAIPlayer = s; + } + + public boolean getCheckAIPlayer() { + return checkAIPlayer; + } + + private void resetCheckAIPlayer() { + checkAIPlayer = false; + } + private int getAtWarCount() { return atWarCount; } @@ -735,11 +801,21 @@ public boolean isSatisfied( } objectiveMet = checkDirectOwnership(listedTerritories, players); } + // check for ai controlled player + if (objectiveMet && getCheckAIPlayer()) { + objectiveMet = checkCheckAIPlayer(players); + } + // check for resources + if (objectiveMet && haveResources != null) { + objectiveMet = checkHaveResources(players); + } // get attached to player final GamePlayer playerAttachedTo = (GamePlayer) getAttachedTo(); + // check for players at war if (objectiveMet && !getAtWarPlayers().isEmpty()) { objectiveMet = checkAtWar(playerAttachedTo, getAtWarPlayers(), getAtWarCount()); } + // check for techs if (objectiveMet && !getTechs().isEmpty()) { objectiveMet = checkTechs(playerAttachedTo, data.getTechnologyFrontier()); } @@ -1001,6 +1077,14 @@ private boolean matchTerritories( return numberMet >= getTerritoryCount(); } + private boolean checkCheckAIPlayer(final List players) { + boolean bcheck = true; + for (GamePlayer player : players) { + bcheck = (bcheck && player.isAi()); + } + return bcheck; + } + private boolean checkAtWar( final GamePlayer player, final Set enemies, final int count) { int found = CollectionUtils.countMatches(enemies, player::isAtWar); @@ -1031,6 +1115,21 @@ private boolean checkTechs(final GamePlayer player, final TechnologyFrontier tec return found >= techCount; } + @VisibleForTesting + public boolean checkHaveResources(final List players) { + final boolean toSum = + haveResources[1].equalsIgnoreCase("sum") || haveResources[1].equalsIgnoreCase("add"); + int itotal = 0; + for (GamePlayer player : players) { + for (int i = toSum ? 2 : 1; i < haveResources.length; i++) { + final Resource resource = getData().getResourceList().getResource(haveResources[i]); + int iamount = player.getResources().getQuantity(resource); + itotal = toSum ? itotal + iamount : Math.max(itotal, iamount); + } + } + return itotal >= getInt(haveResources[0]); + } + @Override public void validate(final GameState data) { validateNames(alliedOwnershipTerritories); @@ -1057,6 +1156,12 @@ public MutableProperty getPropertyOrNull(String propertyName) { this::setRelationship, this::getRelationship, this::resetRelationship); + case "checkAIPlayer": + return MutableProperty.of( + this::setCheckAIPlayer, + this::setCheckAIPlayer, + this::getCheckAIPlayer, + this::resetCheckAIPlayer); case "atWarPlayers": return MutableProperty.of( this::setAtWarPlayers, @@ -1065,6 +1170,12 @@ public MutableProperty getPropertyOrNull(String propertyName) { this::resetAtWarPlayers); case "atWarCount": return MutableProperty.ofReadOnly(this::getAtWarCount); + case "haveResources": + return MutableProperty.of( + this::setHaveResources, + this::setHaveResources, + this::getHaveResources, + this::resetHaveResources); case "destroyedTUV": return MutableProperty.ofString( this::setDestroyedTuv, this::getDestroyedTuv, this::resetDestroyedTuv); diff --git a/game-app/game-core/src/test/java/games/strategy/triplea/attachments/RulesAttachmentTest.java b/game-app/game-core/src/test/java/games/strategy/triplea/attachments/RulesAttachmentTest.java new file mode 100644 index 00000000000..63d16d7235c --- /dev/null +++ b/game-app/game-core/src/test/java/games/strategy/triplea/attachments/RulesAttachmentTest.java @@ -0,0 +1,163 @@ +package games.strategy.triplea.attachments; + +import static games.strategy.triplea.Constants.PUS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import games.strategy.engine.data.GameData; +import games.strategy.engine.data.GamePlayer; +import games.strategy.engine.data.gameparser.GameParseException; +import java.security.SecureRandom; +import java.util.List; + +import games.strategy.triplea.delegate.GameDataTestUtil; +import games.strategy.triplea.xml.TestMapGameData; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class RulesAttachmentTest { + + /** + * "Victory" map is just a branch/mod of Pact of Steel 2. POS2 is an actual game with good gameplay + * that we don't want to mess with, so "Victory" is more of an xml purely for testing purposes, and + * probably should never be played. + */ + private final GameData gameData = TestMapGameData.VICTORY_TEST.getGameData(); + private final RulesAttachment attachment = new RulesAttachment("Test attachment", null, gameData); + + @Nested + class HaveResources { + + private final GamePlayer italians = GameDataTestUtil.italians(gameData); + private final GamePlayer germans = GameDataTestUtil.germans(gameData); + private final String FUEL = "Fuel"; + private final String ORE = "Ore"; + private final String addString = "add"; + private final String sumString ="SUM"; + + /* Length test for haveResources */ + @Test + void setHaveResourcesInvalidLength() { + assertThrows(GameParseException.class, () -> attachment.setHaveResources("")); + assertThrows(GameParseException.class, () -> attachment.setHaveResources(":add")); + + assertThrows(GameParseException.class, () -> attachment.setHaveResources("a")); + assertThrows(GameParseException.class, () -> attachment.setHaveResources("a:add")); + } + + /* Invalid arguments for haveResources */ + @Test + void setHaveResourcesInvalidArgs() { + /* Not a number (NAN) test */ + assertThrows( + IllegalArgumentException.class, + () -> attachment.setHaveResources("NAN:PUs")); + assertThrows( + IllegalArgumentException.class, + () -> attachment.setHaveResources("NAN:add:PUs")); + /* -1 value test */ + assertThrows( + GameParseException.class, + () -> attachment.setHaveResources("0:PUs")); + assertThrows( + GameParseException.class, + () -> attachment.setHaveResources("0:add:PUs")); + /* Not a resource test */ + assertThrows( + GameParseException.class, + () -> attachment.setHaveResources("1:NOT A RESOURCE")); + assertThrows( + GameParseException.class, + () -> attachment.setHaveResources("1:Sum:NOT A RESOURCE")); + assertThrows( + GameParseException.class, () -> attachment.setHaveResources("0:w")); + assertThrows( + GameParseException.class, () -> attachment.setHaveResources("0:w:e")); + assertThrows( + GameParseException.class, () -> attachment.setHaveResources("0:add:w")); + assertThrows( + GameParseException.class, () -> attachment.setHaveResources("0:add:w:e")); + } + + /* Testing stored values with getHaveResources */ + @Test + void setHaveResourcesTest() throws Exception { + final SecureRandom rand = new SecureRandom(); + final String random1 = Integer.toString(Math.abs(rand.nextInt())); + final String[] expected1 = new String[] {random1, PUS}; + + attachment.setHaveResources( + concatWithColon(random1, addString, PUS)); + assertEquals( + expected1[0], + attachment.getHaveResources()[0]); + assertEquals( + expected1[1], + attachment.getHaveResources()[2]); + } + + /* Testing checkHaveResources */ + @Test + void testCheckHaveResources() throws Exception { + final int italianFuelAmount = italians.getResources().getQuantity(FUEL); + final int italianPuAmount = italians.getResources().getQuantity(PUS); + final int italianOreAmount = italians.getResources().getQuantity(ORE); + final int germanFuelAmount = germans.getResources().getQuantity(FUEL); + final int germanPuAmount = germans.getResources().getQuantity(PUS); + final int germanOreAmount = germans.getResources().getQuantity(ORE); + + final int testItalianPU = italianPuAmount; + final int testItalianResources = italianOreAmount + italianFuelAmount + italianPuAmount; + final int testPUs = testItalianPU + germanPuAmount; + final int testResources = testItalianResources + germanPuAmount + germanFuelAmount + germanOreAmount; + + /* testing with 1 player */ + final List players = List.of(italians); + attachment.setHaveResources( + concatWithColon(String.valueOf(testItalianPU), PUS)); + assertTrue( + attachment.checkHaveResources(players)); + attachment.setHaveResources( + concatWithColon(String.valueOf(testItalianResources), addString, PUS)); + assertFalse( + attachment.checkHaveResources(players)); + attachment.setHaveResources( + concatWithColon(String.valueOf(testItalianResources), addString, PUS, FUEL)); + assertFalse( + attachment.checkHaveResources(players)); + attachment.setHaveResources( + concatWithColon(String.valueOf(testItalianResources), addString, PUS, FUEL, ORE)); + assertTrue( + attachment.checkHaveResources(players)); + + /* testing with 2 players */ + final List players1 = List.of(italians, germans); + attachment.setHaveResources( + concatWithColon(String.valueOf(testPUs), sumString, PUS)); + assertTrue( + attachment.checkHaveResources(players1)); + attachment.setHaveResources( + concatWithColon(String.valueOf(testResources), sumString, PUS)); + assertFalse( + attachment.checkHaveResources(players1)); + attachment.setHaveResources( + concatWithColon(String.valueOf(testResources), sumString, PUS, FUEL)); + assertFalse( + attachment.checkHaveResources(players1)); + attachment.setHaveResources( + concatWithColon(String.valueOf(testResources), sumString, PUS, FUEL, ORE)); + assertTrue( + attachment.checkHaveResources(players1)); + + } + @Test + private String concatWithColon(final String... args) { + return String.join(":", args); + } + } +}