Alteration to the CopyCard json functionality

- Added a trigger to CopyCard which automatically unapplies/reapplies the copier when the thing they copy is played
- Added unit tests for Hobbit Farmer extensively checking each use case; in particular modifier sites vs ability sites
- Various minor formatting fixes in other unit tests
- Dare I say it?  "These stupid site copy allies are finally fixed."
This commit is contained in:
Christian 'ketura' McCarty
2023-11-12 15:06:31 -06:00
parent fc5bcd169e
commit dd5003e0ef
10 changed files with 456 additions and 6 deletions

View File

@@ -2,9 +2,13 @@ package com.gempukku.lotro.cards.build.field;
import com.gempukku.lotro.cards.build.InvalidCardDefinitionException;
import com.gempukku.lotro.common.Side;
import org.hjson.JsonValue;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import java.util.Map;
import java.util.Set;
public class FieldUtils {
@@ -106,6 +110,13 @@ public class FieldUtils {
}
}
private static final JSONParser parser = new JSONParser();
public static JSONObject parseSubObject(String jsonString) throws ParseException {
String json = JsonValue.readHjson(jsonString).toString();
var subObject = (JSONObject) parser.parse(json);
return subObject;
}
private static boolean contains(String[] fields, String key) {
for (String field : fields) {
if (field.equals(key))

View File

@@ -6,6 +6,7 @@ import com.gempukku.lotro.cards.build.FilterableSource;
import com.gempukku.lotro.cards.build.InvalidCardDefinitionException;
import com.gempukku.lotro.cards.build.field.EffectProcessor;
import com.gempukku.lotro.cards.build.field.FieldUtils;
import com.gempukku.lotro.cards.build.field.effect.trigger.TriggerChecker;
import org.json.simple.JSONObject;
public class CopyCard implements EffectProcessor {
@@ -15,8 +16,42 @@ public class CopyCard implements EffectProcessor {
final String filter = FieldUtils.getString(value.get("filter"), "filter");
final String autoRefreshTriggerString = """
{
type: trigger
trigger: {
type: played
filter: """ + filter + """
}
effect: {
type: RefreshSelf
}
}
""";
final FilterableSource filterableSource = environment.getFilterFactory().generateFilter(filter, environment);
blueprint.appendCopiedFilter(filterableSource);
//Now we add an automatic trigger that unapplies/reapplies this card so that
// modifiers etc do not get stuck.
try {
var action = FieldUtils.parseSubObject(autoRefreshTriggerString);
var trigger = (JSONObject) action.get("trigger");
final var triggerChecker = environment
.getTriggerCheckerFactory().getTriggerChecker(trigger, environment);
var triggerActionSource = new DefaultActionSource();
triggerActionSource.addPlayRequirement(triggerChecker);
EffectUtils.processRequirementsCostsAndEffects(action, environment, triggerActionSource);
blueprint.appendRequiredAfterTrigger(triggerActionSource);
}
catch(Exception ex) {
throw new InvalidCardDefinitionException("CopyCard could not create auto refresh trigger.", ex);
}
}
}

View File

@@ -111,6 +111,7 @@ public class EffectAppenderFactory {
effectAppenderProducers.put("putstackedcardsintohand", new PutStackedCardsIntoHand());
effectAppenderProducers.put("reconcilehand", new ReconcileHand());
effectAppenderProducers.put("reducearcherytotal", new ReduceArcheryTotal());
effectAppenderProducers.put("refreshself", new RefreshSelf());
effectAppenderProducers.put("reinforcetokens", new ReinforceTokens());
effectAppenderProducers.put("removeburdens", new RemoveBurdens());
effectAppenderProducers.put("removecardsindiscardfromgame", new RemoveCardsInDiscardFromGame());

View File

@@ -0,0 +1,38 @@
package com.gempukku.lotro.cards.build.field.effect.appender;
import com.gempukku.lotro.cards.build.ActionContext;
import com.gempukku.lotro.cards.build.CardGenerationEnvironment;
import com.gempukku.lotro.cards.build.InvalidCardDefinitionException;
import com.gempukku.lotro.cards.build.field.FieldUtils;
import com.gempukku.lotro.cards.build.field.effect.EffectAppender;
import com.gempukku.lotro.cards.build.field.effect.EffectAppenderProducer;
import com.gempukku.lotro.game.state.LotroGame;
import com.gempukku.lotro.logic.actions.CostToEffectAction;
import com.gempukku.lotro.logic.effects.PutOnTheOneRingEffect;
import com.gempukku.lotro.logic.timing.Effect;
import com.gempukku.lotro.logic.timing.PlayConditions;
import com.gempukku.lotro.logic.timing.UnrespondableEffect;
import org.json.simple.JSONObject;
public class RefreshSelf implements EffectAppenderProducer {
@Override
public EffectAppender createEffectAppender(JSONObject effectObject, CardGenerationEnvironment environment) throws InvalidCardDefinitionException {
FieldUtils.validateAllowedFields(effectObject);
//This is intended to be used with cards using CopyCard, such as Hobbit Farmer.
// This very well might have side-effects including removing modifiers applied this turn.
return new DelayedAppender() {
@Override
protected Effect createEffect(boolean cost, CostToEffectAction action, ActionContext ac) {
return new UnrespondableEffect() {
@Override
protected void doPlayEffect(LotroGame game) {
ac.getGame().getGameState().reapplyAffectingForCard(ac.getGame(), ac.getSource());
}
};
}
};
}
}

View File

@@ -637,6 +637,16 @@ public class GenericCardTestHelper extends AbstractAtTest {
SkipToPhase(Phase.ASSIGNMENT);
PassCurrentPhaseActions();
}
public void SkipToMovementDecision() throws DecisionResultInvalidException {
SkipToPhase(Phase.REGROUP);
PassCurrentPhaseActions();
if(ShadowDecisionAvailable("reconcile")) {
ShadowDeclineReconciliation();
}
while(ShadowDecisionAvailable("discard down")) {
ShadowChooseCard((PhysicalCardImpl) GetShadowHand().get(0));
}
}
public void SkipToPhase(Phase target) throws DecisionResultInvalidException {
for(int attempts = 1; attempts <= 20; attempts++)
{
@@ -1040,7 +1050,7 @@ public class GenericCardTestHelper extends AbstractAtTest {
{
ShadowDeclineReconciliation();
}
if(ShadowDecisionAvailable("discard down"))
while(ShadowDecisionAvailable("discard down"))
{
ShadowChooseCard((PhysicalCardImpl) GetShadowHand().get(0));
}

View File

@@ -1,8 +1,7 @@
package com.gempukku.lotro.cards.official.set01;
import com.gempukku.lotro.cards.GenericCardTestHelper;
import com.gempukku.lotro.common.Phase;
import com.gempukku.lotro.common.SitesBlock;
import com.gempukku.lotro.common.*;
import com.gempukku.lotro.game.CardNotFoundException;
import com.gempukku.lotro.game.PhysicalCardImpl;
import com.gempukku.lotro.logic.decisions.DecisionResultInvalidException;
@@ -72,6 +71,10 @@ public class Card_01_040_Tests
PhysicalCardImpl elrond = scn.GetFreepsCard("elrond");
assertTrue(elrond.getBlueprint().isUnique());
assertEquals(Side.FREE_PEOPLE, elrond.getBlueprint().getSide());
assertEquals(Culture.ELVEN, elrond.getBlueprint().getCulture());
assertEquals(CardType.ALLY, elrond.getBlueprint().getCardType());
assertEquals(Race.ELF, elrond.getBlueprint().getRace());
assertEquals(4, elrond.getBlueprint().getTwilightCost());
assertEquals(8, elrond.getBlueprint().getStrength());
assertEquals(4, elrond.getBlueprint().getVitality());

View File

@@ -0,0 +1,347 @@
package com.gempukku.lotro.cards.official.set01;
import com.gempukku.lotro.cards.GenericCardTestHelper;
import com.gempukku.lotro.common.*;
import com.gempukku.lotro.game.CardNotFoundException;
import com.gempukku.lotro.game.PhysicalCardImpl;
import com.gempukku.lotro.logic.decisions.DecisionResultInvalidException;
import org.junit.Test;
import java.util.HashMap;
import static org.junit.Assert.*;
public class Card_01_295_Tests
{
protected GenericCardTestHelper GetScenario() throws CardNotFoundException, DecisionResultInvalidException {
return new GenericCardTestHelper(
new HashMap<>()
{{
put("farmer", "1_295");
put("farmer2", "1_295");
put("merry", "4_310");
put("pippin", "4_314");
}},
new HashMap<>() {{
put("site1", "1_321");
put("site2", "1_327");
put("site3", "1_337");
put("site4", "1_343");
put("site5", "1_349");
put("site6", "1_350");
put("site7", "1_353");
put("site8", "1_356");
put("site9", "1_360");
}},
GenericCardTestHelper.FOTRFrodo,
GenericCardTestHelper.RulingRing
);
}
protected GenericCardTestHelper GetDiscountScenario() throws CardNotFoundException, DecisionResultInvalidException {
return new GenericCardTestHelper(
new HashMap<>()
{{
put("farmer", "1_295");
put("farmer2", "1_295");
put("merry", "4_310");
put("pippin", "4_314");
put("sam", "1_311");
}},
new HashMap<>() {{
put("site1", "1_323");
put("site2", "1_327");
put("site3", "1_337");
put("site4", "1_343");
put("site5", "1_349");
put("site6", "1_350");
put("site7", "1_353");
put("site8", "1_356");
put("site9", "1_360");
}},
GenericCardTestHelper.FOTRFrodo,
GenericCardTestHelper.RulingRing
);
}
@Test
public void FarmerStatsAndKeywordsAreCorrect() throws DecisionResultInvalidException, CardNotFoundException {
/**
* Set: 1
* Title: Hobbit Farmer
* Unique: false
* Side: Free Peoples
* Culture: Shire
* Twilight Cost: 1
* Type: Ally
* Race: Hobbit
* Game Text: While you can spot your site 1, this ally has the game text of that site.
* Fellowship: Exert this ally and spot your opponent's site 1 to replace it with your site 1.
*/
//Pre-game setup
var scn = GetScenario();
var farmer = scn.GetFreepsCard("farmer");
assertFalse(farmer.getBlueprint().isUnique());
assertEquals(Side.FREE_PEOPLE, farmer.getBlueprint().getSide());
assertEquals(Culture.SHIRE, farmer.getBlueprint().getCulture());
assertEquals(CardType.ALLY, farmer.getBlueprint().getCardType());
assertEquals(Race.HOBBIT, farmer.getBlueprint().getRace());
assertEquals(1, farmer.getBlueprint().getTwilightCost());
assertEquals(2, farmer.getBlueprint().getStrength());
assertEquals(2, farmer.getBlueprint().getVitality());
assertEquals(1, farmer.getBlueprint().getAllyHomeSiteNumbers()[0]);
assertEquals(SitesBlock.FELLOWSHIP, farmer.getBlueprint().getAllyHomeSiteBlock());
}
@Test
public void FarmerCopiesAbilitiesOnOwnSite1ButNotOpponentSite1() throws DecisionResultInvalidException, CardNotFoundException {
//Pre-game setup
var scn = GetScenario();
//Player 1 version of the Farmer will test copying one's own site
var freepsFarmer = scn.GetFreepsCard("farmer");
var merry = scn.GetFreepsCard("merry");
var pippin = scn.GetFreepsCard("pippin");
scn.FreepsMoveCardToSupportArea(freepsFarmer);
scn.AddWoundsToChar(freepsFarmer, 1); // To ensure the exert ability isn't available
scn.FreepsMoveCardsToTopOfDeck(merry, pippin);
//Player 2 version of the Farmer will test copying the enemy site
var evilFarmer = scn.GetShadowCard("farmer");
var evilMerry = scn.GetShadowCard("merry");
var evilPippin = scn.GetShadowCard("pippin");
scn.ShadowMoveCardToSupportArea(evilFarmer);
scn.AddWoundsToChar(evilFarmer, 1); // To ensure the exert ability isn't available
scn.StartGame();
assertTrue(scn.FreepsActionAvailable("Farmer Maggot's Fields"));
assertTrue(scn.FreepsActionAvailable("Use Hobbit Farmer"));
scn.SkipToMovementDecision();
scn.ShadowMoveCardsToTopOfDeck(evilMerry, evilPippin);
scn.FreepsChooseToStay();
assertEquals(Phase.FELLOWSHIP, scn.GetCurrentPhase());
assertEquals(1, scn.GetVitality(evilFarmer));
//Since the site is player 1's, player 2 should not have access to it via Hobbit Farmer, only natively
assertTrue(scn.ShadowActionAvailable("Farmer Maggot's Fields"));
assertFalse(scn.ShadowActionAvailable("Use Hobbit Farmer"));
}
@Test
public void FarmerCopiesModifiersOnOwnSite1ButNotOpponentSite1() throws DecisionResultInvalidException, CardNotFoundException {
//Pre-game setup
var scn = GetDiscountScenario();
//Player 1 version of the Farmer will test copying one's own site
var freepsFarmer = scn.GetFreepsCard("farmer");
var sam = scn.GetFreepsCard("sam");
scn.FreepsMoveCardToSupportArea(freepsFarmer);
scn.FreepsMoveCardToHand(sam);
//Player 2 version of the Farmer will test copying the enemy site
var evilFarmer = scn.GetShadowCard("farmer");
var evilSam = scn.GetShadowCard("sam");
scn.ShadowMoveCardToSupportArea(evilFarmer);
scn.ShadowMoveCardToHand(evilSam);
scn.StartGame();
assertEquals(2, sam.getBlueprint().getTwilightCost());
assertEquals(0, scn.GetTwilight());
scn.FreepsPlayCard(sam);
//Both the Green Hill Country and Hobbit Farmer should have stacked to make Sam -2.
assertEquals(0, scn.GetTwilight());
scn.SkipToMovementDecision();
scn.FreepsChooseToStay();
assertEquals(Phase.FELLOWSHIP, scn.GetCurrentPhase());
//Since the site is player 1's, player 2 should not have access to it via Hobbit Farmer, only natively
assertEquals(2, evilSam.getBlueprint().getTwilightCost());
assertEquals(0, scn.GetTwilight());
scn.ShadowPlayCard(evilSam);
assertEquals(1, scn.GetTwilight());
}
@Test
public void FarmerAbilityExertsToReplaceOpponentsSite1() throws DecisionResultInvalidException, CardNotFoundException {
//Pre-game setup
var scn = GetScenario();
var opponentSite1 = scn.GetCurrentSite();
scn.FreepsMoveCardToSupportArea("farmer");
//Player 2 version of the Farmer will test replacing
var evilFarmer = scn.GetShadowCard("farmer");
var evilMerry = scn.GetShadowCard("merry");
var evilPippin = scn.GetShadowCard("pippin");
var site1 = scn.GetShadowSite("site1");
scn.ShadowMoveCardToSupportArea(evilFarmer);
scn.StartGame();
assertFalse(scn.FreepsDecisionAvailable("Use Hobbit Farmer"));
scn.SkipToMovementDecision();
scn.ShadowMoveCardsToTopOfDeck(evilMerry, evilPippin);
scn.FreepsChooseToStay();
assertEquals(Phase.FELLOWSHIP, scn.GetCurrentPhase());
assertSame(opponentSite1, scn.GetCurrentSite());
assertEquals(Zone.ADVENTURE_DECK, site1.getZone());
assertTrue(scn.ShadowActionAvailable(evilFarmer));
assertEquals(2, scn.GetVitality(evilFarmer));
scn.ShadowUseCardAction(evilFarmer);
assertSame(site1, scn.GetCurrentSite());
assertEquals(Zone.ADVENTURE_DECK, opponentSite1.getZone());
assertEquals(1, scn.GetVitality(evilFarmer));
//Now that we have replaced the site, the Hobbit Farmer should now automatically be copying it.
assertTrue(scn.ShadowActionAvailable("Farmer Maggot's Fields"));
assertTrue(scn.ShadowActionAvailable("Use Hobbit Farmer"));
}
@Test
public void FarmerImmediatelyCopiesAbilitiesAfterReplacingDownThePath() throws DecisionResultInvalidException, CardNotFoundException {
//Pre-game setup
var scn = GetScenario();
var opponentSite1 = scn.GetCurrentSite();
scn.FreepsMoveCardToSupportArea("farmer");
//Player 2 version of the Farmer will test replacing
var evilFarmer = scn.GetShadowCard("farmer");
var evilMerry = scn.GetShadowCard("merry");
var evilPippin = scn.GetShadowCard("pippin");
var site1 = scn.GetShadowSite("site1");
scn.ShadowMoveCardToSupportArea(evilFarmer);
scn.StartGame();
assertFalse(scn.FreepsDecisionAvailable("Use Hobbit Farmer"));
//Getting everyone far away from site 1
scn.SkipToSite(3);
scn.SkipToMovementDecision();
scn.ShadowMoveCardsToTopOfDeck(evilMerry, evilPippin);
scn.FreepsChooseToStay();
scn.FreepsDeclineReconciliation();
assertEquals(Phase.FELLOWSHIP, scn.GetCurrentPhase());
assertEquals(Zone.ADVENTURE_PATH, opponentSite1.getZone());
assertEquals(Zone.ADVENTURE_DECK, site1.getZone());
assertTrue(scn.ShadowActionAvailable(evilFarmer));
assertEquals(2, scn.GetVitality(evilFarmer));
scn.ShadowUseCardAction(evilFarmer);
assertEquals(Zone.ADVENTURE_PATH, site1.getZone());
assertEquals(Zone.ADVENTURE_DECK, opponentSite1.getZone());
assertEquals(1, scn.GetVitality(evilFarmer));
//Now that we have replaced the site, the Hobbit Farmer should now automatically be copying it.
assertTrue(scn.ShadowActionAvailable("Use Hobbit Farmer"));
}
@Test
public void FarmerCopiesModifiersDownThePath() throws DecisionResultInvalidException, CardNotFoundException {
//Pre-game setup
var scn = GetDiscountScenario();
var farmer = scn.GetFreepsCard("farmer");
var merry = scn.GetFreepsCard("merry");
var pippin = scn.GetFreepsCard("pippin");
var site1 = scn.GetCurrentSite();
scn.FreepsMoveCardToSupportArea("farmer");
scn.FreepsMoveCardToHand(merry, pippin);
var opponentSite1 = scn.GetShadowSite("site1");
scn.StartGame();
//Ensuring Green Hill Country's -1 twilight discount is working natively.
assertEquals(1, pippin.getBlueprint().getTwilightCost());
assertEquals(0, scn.GetTwilight());
scn.FreepsPlayCard(pippin);
assertEquals(0, scn.GetTwilight());
//Getting everyone far away from site 1
scn.SkipToSite(3);
assertEquals(Phase.FELLOWSHIP, scn.GetCurrentPhase());
assertEquals(Zone.ADVENTURE_DECK, opponentSite1.getZone());
assertEquals(Zone.ADVENTURE_PATH, site1.getZone());
//We've never replaced the site, but the Hobbit Farmer should still automatically be copying the twilight discount.
assertEquals(1, merry.getBlueprint().getTwilightCost());
assertEquals(0, scn.GetTwilight());
scn.FreepsPlayCard(merry);
assertEquals(0, scn.GetTwilight());
}
@Test
public void FarmerImmediatelyCopiesModifiersAfterReplacingDownThePath() throws DecisionResultInvalidException, CardNotFoundException {
//Pre-game setup
var scn = GetDiscountScenario();
var opponentSite1 = scn.GetCurrentSite();
scn.FreepsMoveCardToSupportArea("farmer");
//Player 2 version of the Farmer will test replacing
var evilFarmer = scn.GetShadowCard("farmer");
var evilMerry = scn.GetShadowCard("merry");
var site1 = scn.GetShadowSite("site1");
scn.ShadowMoveCardToSupportArea(evilFarmer);
scn.StartGame();
assertFalse(scn.FreepsDecisionAvailable("Use Hobbit Farmer"));
//Getting everyone far away from site 1
scn.SkipToSite(3);
scn.SkipToMovementDecision();
scn.FreepsChooseToStay();
scn.FreepsDeclineReconciliation();
assertEquals(Phase.FELLOWSHIP, scn.GetCurrentPhase());
assertEquals(Zone.ADVENTURE_PATH, opponentSite1.getZone());
assertEquals(Zone.ADVENTURE_DECK, site1.getZone());
assertTrue(scn.ShadowActionAvailable(evilFarmer));
assertEquals(2, scn.GetVitality(evilFarmer));
scn.ShadowUseCardAction(evilFarmer);
assertEquals(Zone.ADVENTURE_PATH, site1.getZone());
assertEquals(Zone.ADVENTURE_DECK, opponentSite1.getZone());
assertEquals(1, scn.GetVitality(evilFarmer));
//Now that we have replaced the site, the Hobbit Farmer should now automatically be copying the twilight discount.
assertEquals(1, evilMerry.getBlueprint().getTwilightCost());
assertEquals(0, scn.GetTwilight());
scn.ShadowPlayCard(evilMerry);
assertEquals(0, scn.GetTwilight());
}
}

View File

@@ -27,6 +27,7 @@ public class Card_03_068_ErrataTests
);
}
@Test
public void SarumanStatsAndKeywordsAreCorrect() throws DecisionResultInvalidException, CardNotFoundException {
/**
@@ -59,7 +60,7 @@ public class Card_03_068_ErrataTests
assertEquals(4, saruman.getBlueprint().getTwilightCost());
assertEquals(8, saruman.getBlueprint().getStrength());
assertEquals(4, saruman.getBlueprint().getVitality());
assertEquals(4, saruman.getBlueprint().getSiteNumber()); // Change this to getAllyHomeSiteNumbers for allies
assertEquals(4, saruman.getBlueprint().getSiteNumber());
}
@Test

View File

@@ -45,7 +45,9 @@ public class Card_03_090_ErrataTests
* Twilight Cost: 0
* Type: event
* Subtype: Maneuver
* Game Text: Exert a unique [sauron] minion to exert a companion (except the Ring-bearer) and discard a possession attached to that companion. The Free Peoples player may discard 1 hand at random from their hand to prevent this.
* Game Text: Exert a unique [sauron] minion to exert a companion (except the Ring-bearer) and
* discard a possession attached to that companion. The Free Peoples player may discard 1
* card at random from their hand to prevent this.
*/
//Pre-game setup

View File

@@ -65,7 +65,9 @@ public class Card_03_103_ErrataTests
* Twilight Cost: 0
* Type: event
* Subtype:
* Game Text: <b>Maneuver:</b> Spot a [sauron] minion to wound Galadriel 3 times. The Free Peoples player may discard 2 Elves to prevent this. If Galadriel is Ring-bound, 2 burdens may be added instead.
* Game Text: <b>Maneuver:</b> Spot a [sauron] minion to wound Galadriel 3 times.
* The Free Peoples player may discard 2 Elves to prevent this. If Galadriel is
* Ring-bound, 2 burdens may be added instead.
*/
//Pre-game setup