From 763b7a8dafcbd7fd89012ceeaf0e6108ee60b531 Mon Sep 17 00:00:00 2001
From: WCSumpton <wc_sumpton@hotmail.com>
Date: Mon, 18 Sep 2023 09:28:07 -0400
Subject: [PATCH] haveResources w/testing and checkAIPlayer

Adding haveResources and checkAIPlayer to RulesAttachment. Included is RulesAttachmentTest which uses "VICTORY_TEST" to test haveResources. checkAIPlayer is not tested because of internal testing for boolean values.

Cheers...
---
 .../triplea/attachments/RulesAttachment.java  | 111 ++++++++++++
 .../attachments/RulesAttachmentTest.java      | 163 ++++++++++++++++++
 2 files changed, 274 insertions(+)
 create mode 100644 game-app/game-core/src/test/java/games/strategy/triplea/attachments/RulesAttachmentTest.java

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<String> relationship = null;
+  // condition for checking ai player
+  private boolean checkAIPlayer = false;
   // condition for being at war
   private @Nullable Set<GamePlayer> 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<RulesAttachment> 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<GamePlayer> players) {
+    boolean bcheck = true;
+    for (GamePlayer player : players) {
+      bcheck = (bcheck && player.isAi());
+    }
+    return bcheck;
+  }
+
   private boolean checkAtWar(
       final GamePlayer player, final Set<GamePlayer> 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<GamePlayer> 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<GamePlayer> 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<GamePlayer> 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);
+    }
+  }
+}