From 9e6c903d6c24cfb7c7fa54241975ab9968538a25 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 07:35:07 +0000 Subject: [PATCH 1/9] Track known Yssaril action cards Agent-Logs-Url: https://github.com/AsyncTI4/TI4_map_generator_bot/sessions/15211ef4-d6b5-4c43-83ab-167767d7ae01 Co-authored-by: wholton <3317595+wholton@users.noreply.github.com> --- .../handlers/info/CardsInfoButtonHandler.java | 10 ++ src/main/java/ti4/game/Player.java | 2 + .../java/ti4/helpers/ActionCardHelper.java | 2 + .../actioncard/KnownActionCardsService.java | 86 ++++++++++++++++++ .../ti4/service/info/CardsInfoService.java | 4 + .../KnownActionCardsServiceTest.java | 91 +++++++++++++++++++ 6 files changed, 195 insertions(+) create mode 100644 src/main/java/ti4/service/actioncard/KnownActionCardsService.java create mode 100644 src/test/java/ti4/service/actioncard/KnownActionCardsServiceTest.java diff --git a/src/main/java/ti4/discord/interactions/buttons/handlers/info/CardsInfoButtonHandler.java b/src/main/java/ti4/discord/interactions/buttons/handlers/info/CardsInfoButtonHandler.java index 97fe28fe40..510a19b7d9 100644 --- a/src/main/java/ti4/discord/interactions/buttons/handlers/info/CardsInfoButtonHandler.java +++ b/src/main/java/ti4/discord/interactions/buttons/handlers/info/CardsInfoButtonHandler.java @@ -8,6 +8,8 @@ import ti4.game.Game; import ti4.game.Player; import ti4.logging.BotLogger; +import ti4.message.MessageHelper; +import ti4.service.actioncard.KnownActionCardsService; import ti4.service.breakthrough.EidolonMaximumService; import ti4.service.info.CardsInfoService; @@ -31,4 +33,12 @@ public static void sendCardsInfo(Game game, Player player, GenericInteractionCre CardsInfoService.sendCardsInfo(game, player, event); EidolonMaximumService.sendEidolonMaximumFlipButtons(game, player); } + + @ButtonHandler(value = "showKnownActionCards", save = false) + public static void showKnownActionCards(Game game, Player player) { + if (player == null) { + return; + } + MessageHelper.sendMessageToPlayerCardsInfoThread(player, KnownActionCardsService.getKnownActionCardsText(game, player)); + } } diff --git a/src/main/java/ti4/game/Player.java b/src/main/java/ti4/game/Player.java index cd1e5cbf2a..61c5f3dda7 100644 --- a/src/main/java/ti4/game/Player.java +++ b/src/main/java/ti4/game/Player.java @@ -85,6 +85,7 @@ import ti4.model.TechnologyModel.TechnologyType; import ti4.model.TemporaryCombatModifierModel; import ti4.model.UnitModel; +import ti4.service.actioncard.KnownActionCardsService; import ti4.service.agenda.IsPlayerElectedService; import ti4.service.breakthrough.DeepgloomService; import ti4.service.breakthrough.ValefarZService; @@ -1246,6 +1247,7 @@ public void removeActionCard(Integer identifier) { } } actionCards.remove(idToRemove); + KnownActionCardsService.forgetCardFromKnownHands(game, this, idToRemove); } public void removePromissoryNote(Integer identifier) { diff --git a/src/main/java/ti4/helpers/ActionCardHelper.java b/src/main/java/ti4/helpers/ActionCardHelper.java index 1ce8b032b4..740bc88f1c 100644 --- a/src/main/java/ti4/helpers/ActionCardHelper.java +++ b/src/main/java/ti4/helpers/ActionCardHelper.java @@ -37,6 +37,7 @@ import ti4.model.UnitModel; import ti4.model.metadata.AutoPingMetadataManager; import ti4.service.actioncard.SabotageService; +import ti4.service.actioncard.KnownActionCardsService; import ti4.service.agenda.IsPlayerElectedService; import ti4.service.breakthrough.DeepgloomService; import ti4.service.emoji.CardEmojis; @@ -2067,6 +2068,7 @@ public static void showAll(Player player, Player player_, Game game) { } MessageHelper.sendMessageToPlayerCardsInfoThread(player, sa.toString()); MessageHelper.sendMessageToPlayerCardsInfoThread(player_, sb.toString()); + KnownActionCardsService.rememberViewedHand(player_, player); } public static String actionCardListCondensedNoIds(List discards, String title) { diff --git a/src/main/java/ti4/service/actioncard/KnownActionCardsService.java b/src/main/java/ti4/service/actioncard/KnownActionCardsService.java new file mode 100644 index 0000000000..cec96c8136 --- /dev/null +++ b/src/main/java/ti4/service/actioncard/KnownActionCardsService.java @@ -0,0 +1,86 @@ +package ti4.service.actioncard; + +import java.util.ArrayList; +import java.util.List; +import lombok.experimental.UtilityClass; +import ti4.game.Game; +import ti4.game.Player; +import ti4.helpers.ActionCardHelper; + +@UtilityClass +public class KnownActionCardsService { + + private static final String KNOWN_ACTION_CARDS_PREFIX = "knownActionCards_"; + + public static void rememberViewedHand(Player viewer, Player target) { + if (viewer == null || target == null || viewer == target) { + return; + } + + String storageKey = getStorageKey(target); + viewer.removeStoredValue(storageKey); + List knownCards = new ArrayList<>(target.getActionCards().keySet()); + if (!knownCards.isEmpty()) { + viewer.addToStoredList(storageKey, knownCards.toArray(String[]::new)); + } + } + + public static void forgetCardFromKnownHands(Game game, Player target, String actionCardId) { + if (game == null || target == null || actionCardId == null || actionCardId.isBlank()) { + return; + } + + String storageKey = getStorageKey(target); + for (Player player : game.getPlayers().values()) { + if (player == target || !player.hasStoredValue(storageKey)) { + continue; + } + player.removeFromStoredList(storageKey, actionCardId); + } + } + + public static boolean shouldShowKnownActionCardsButton(Player player) { + return player != null + && (player.hasTech("mi") + || player.getExhaustedTechs().contains("mi") + || player.getStoredValueMap().keySet().stream() + .anyMatch(key -> key.startsWith(KNOWN_ACTION_CARDS_PREFIX))); + } + + public static String getKnownActionCardsText(Game game, Player viewer) { + StringBuilder sb = new StringBuilder("__Known action cards in other players' hands__:"); + boolean hasAnyKnownCards = false; + + for (Player target : game.getRealPlayers()) { + if (target == viewer) { + continue; + } + + List knownCards = viewer.getStoredList(getStorageKey(target)); + if (knownCards.isEmpty()) { + continue; + } + + hasAnyKnownCards = true; + sb.append("\n### ").append(target.getRepresentationNoPing()).append('\n'); + sb.append(ActionCardHelper.actionCardListCondensedNoIds(knownCards, null)).append('\n'); + } + + if (!hasAnyKnownCards) { + sb.append("\n> None"); + } + + return sb.toString().trim(); + } + + private static String getStorageKey(Player target) { + String targetId = target.getFaction(); + if (targetId == null || targetId.isBlank() || "null".equalsIgnoreCase(targetId)) { + targetId = target.getColor(); + } + if (targetId == null || targetId.isBlank() || "null".equalsIgnoreCase(targetId)) { + targetId = target.getUserID(); + } + return KNOWN_ACTION_CARDS_PREFIX + targetId; + } +} diff --git a/src/main/java/ti4/service/info/CardsInfoService.java b/src/main/java/ti4/service/info/CardsInfoService.java index 1ea255297f..9a3d7fceba 100644 --- a/src/main/java/ti4/service/info/CardsInfoService.java +++ b/src/main/java/ti4/service/info/CardsInfoService.java @@ -17,6 +17,7 @@ import ti4.helpers.PromissoryNoteHelper; import ti4.logging.BotLogger; import ti4.message.MessageHelper; +import ti4.service.actioncard.KnownActionCardsService; import ti4.service.agenda.IsPlayerElectedService; import ti4.service.emoji.CardEmojis; import ti4.service.emoji.FactionEmojis; @@ -169,6 +170,9 @@ public static void sendVariousAdditionalButtons(Game game, Player player) { if (player.hasUnlockedBreakthrough("yssarilbt")) { buttons.add(Buttons.green("startYssarilbt", "Use Yssaril Breakthrough", FactionEmojis.Yssaril)); } + if (KnownActionCardsService.shouldShowKnownActionCardsButton(player)) { + buttons.add(Buttons.gray("showKnownActionCards", "Show Known Action Cards", CardEmojis.ActionCard)); + } if (player.hasAbility("pillage") && !game.isTwilightsFallMode()) { if (game.getStoredValue("willPillageOwnTransactions" + player.getFaction()) .isEmpty()) { diff --git a/src/test/java/ti4/service/actioncard/KnownActionCardsServiceTest.java b/src/test/java/ti4/service/actioncard/KnownActionCardsServiceTest.java new file mode 100644 index 0000000000..d13c2072fc --- /dev/null +++ b/src/test/java/ti4/service/actioncard/KnownActionCardsServiceTest.java @@ -0,0 +1,91 @@ +package ti4.service.actioncard; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.LinkedHashMap; +import java.util.List; +import org.junit.jupiter.api.Test; +import ti4.game.Game; +import ti4.game.Player; +import ti4.testUtils.BaseTi4Test; + +class KnownActionCardsServiceTest extends BaseTi4Test { + + @Test + void rememberViewedHandAndForgetRemovedCards() { + Game game = new Game(); + + Player viewer = createPlayer(game, "viewer", "yssaril", "blue"); + Player target = createPlayer(game, "target", "hacan", "yellow"); + Player other = createPlayer(game, "other", "saar", "black"); + + target.setActionCard("direct_hit1", 11); + target.setActionCard("skilled_retreat1", 12); + target.setActionCard("skilled_retreat2", 13); + + LinkedHashMap players = new LinkedHashMap<>(); + players.put(viewer.getUserID(), viewer); + players.put(target.getUserID(), target); + players.put(other.getUserID(), other); + game.setPlayers(players); + + KnownActionCardsService.rememberViewedHand(viewer, target); + + assertThat(viewer.getStoredList("knownActionCards_hacan")) + .containsExactlyInAnyOrder("direct_hit1", "skilled_retreat1", "skilled_retreat2"); + + target.removeActionCard(12); + + assertThat(viewer.getStoredList("knownActionCards_hacan")) + .containsExactlyInAnyOrder("direct_hit1", "skilled_retreat2"); + } + + @Test + void getKnownActionCardsTextGroupsByPlayerAndCardName() { + Game game = new Game(); + + Player viewer = createPlayer(game, "viewer", "yssaril", "blue"); + Player firstTarget = createPlayer(game, "first", "hacan", "yellow"); + Player secondTarget = createPlayer(game, "second", "saar", "black"); + + LinkedHashMap players = new LinkedHashMap<>(); + players.put(viewer.getUserID(), viewer); + players.put(firstTarget.getUserID(), firstTarget); + players.put(secondTarget.getUserID(), secondTarget); + game.setPlayers(players); + + viewer.addToStoredList("knownActionCards_hacan", "direct_hit1", "skilled_retreat1"); + viewer.addToStoredList("knownActionCards_saar", "skilled_retreat2"); + + String text = KnownActionCardsService.getKnownActionCardsText(game, viewer); + + assertThat(text).contains("Known action cards in other players' hands"); + assertThat(text).contains("hacan"); + assertThat(text).contains("saar"); + assertThat(text).contains("Direct Hit"); + assertThat(text).contains("Skilled Retreat"); + } + + @Test + void shouldShowKnownActionCardsButtonForMageonOwnersOrTrackedKnowledge() { + Game game = new Game(); + Player viewer = createPlayer(game, "viewer", "yssaril", "blue"); + + assertThat(KnownActionCardsService.shouldShowKnownActionCardsButton(viewer)).isFalse(); + + viewer.setTechs(List.of("mi")); + assertThat(KnownActionCardsService.shouldShowKnownActionCardsButton(viewer)).isTrue(); + + viewer.setTechs(List.of()); + viewer.addToStoredList("knownActionCards_hacan", "direct_hit1"); + assertThat(KnownActionCardsService.shouldShowKnownActionCardsButton(viewer)).isTrue(); + } + + private Player createPlayer(Game game, String userId, String faction, String color) { + Player player = new Player(userId, userId, game); + player.setFaction(faction); + player.setColor(color); + player.setUserName(userId); + return player; + } +} From f807e83693dde82a6f9369a6180a9431b08d6dc1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 07:41:20 +0000 Subject: [PATCH 2/9] Finalize known action card tracking Agent-Logs-Url: https://github.com/AsyncTI4/TI4_map_generator_bot/sessions/15211ef4-d6b5-4c43-83ab-167767d7ae01 Co-authored-by: wholton <3317595+wholton@users.noreply.github.com> --- .../handlers/info/CardsInfoButtonHandler.java | 3 +- .../java/ti4/helpers/ActionCardHelper.java | 2 +- .../actioncard/KnownActionCardsService.java | 5 +++- .../KnownActionCardsServiceTest.java | 30 ++++++++++--------- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/main/java/ti4/discord/interactions/buttons/handlers/info/CardsInfoButtonHandler.java b/src/main/java/ti4/discord/interactions/buttons/handlers/info/CardsInfoButtonHandler.java index 510a19b7d9..b2d37ad4d3 100644 --- a/src/main/java/ti4/discord/interactions/buttons/handlers/info/CardsInfoButtonHandler.java +++ b/src/main/java/ti4/discord/interactions/buttons/handlers/info/CardsInfoButtonHandler.java @@ -39,6 +39,7 @@ public static void showKnownActionCards(Game game, Player player) { if (player == null) { return; } - MessageHelper.sendMessageToPlayerCardsInfoThread(player, KnownActionCardsService.getKnownActionCardsText(game, player)); + MessageHelper.sendMessageToPlayerCardsInfoThread( + player, KnownActionCardsService.getKnownActionCardsText(game, player)); } } diff --git a/src/main/java/ti4/helpers/ActionCardHelper.java b/src/main/java/ti4/helpers/ActionCardHelper.java index 740bc88f1c..29be3194cf 100644 --- a/src/main/java/ti4/helpers/ActionCardHelper.java +++ b/src/main/java/ti4/helpers/ActionCardHelper.java @@ -36,8 +36,8 @@ import ti4.model.TemporaryCombatModifierModel; import ti4.model.UnitModel; import ti4.model.metadata.AutoPingMetadataManager; -import ti4.service.actioncard.SabotageService; import ti4.service.actioncard.KnownActionCardsService; +import ti4.service.actioncard.SabotageService; import ti4.service.agenda.IsPlayerElectedService; import ti4.service.breakthrough.DeepgloomService; import ti4.service.emoji.CardEmojis; diff --git a/src/main/java/ti4/service/actioncard/KnownActionCardsService.java b/src/main/java/ti4/service/actioncard/KnownActionCardsService.java index cec96c8136..4301e2137f 100644 --- a/src/main/java/ti4/service/actioncard/KnownActionCardsService.java +++ b/src/main/java/ti4/service/actioncard/KnownActionCardsService.java @@ -6,6 +6,7 @@ import ti4.game.Game; import ti4.game.Player; import ti4.helpers.ActionCardHelper; +import ti4.helpers.StringHelper; @UtilityClass public class KnownActionCardsService { @@ -44,6 +45,7 @@ public static boolean shouldShowKnownActionCardsButton(Player player) { && (player.hasTech("mi") || player.getExhaustedTechs().contains("mi") || player.getStoredValueMap().keySet().stream() + .map(StringHelper::unescape) .anyMatch(key -> key.startsWith(KNOWN_ACTION_CARDS_PREFIX))); } @@ -63,7 +65,8 @@ public static String getKnownActionCardsText(Game game, Player viewer) { hasAnyKnownCards = true; sb.append("\n### ").append(target.getRepresentationNoPing()).append('\n'); - sb.append(ActionCardHelper.actionCardListCondensedNoIds(knownCards, null)).append('\n'); + sb.append(ActionCardHelper.actionCardListCondensedNoIds(knownCards, null)) + .append('\n'); } if (!hasAnyKnownCards) { diff --git a/src/test/java/ti4/service/actioncard/KnownActionCardsServiceTest.java b/src/test/java/ti4/service/actioncard/KnownActionCardsServiceTest.java index d13c2072fc..2acb276363 100644 --- a/src/test/java/ti4/service/actioncard/KnownActionCardsServiceTest.java +++ b/src/test/java/ti4/service/actioncard/KnownActionCardsServiceTest.java @@ -19,9 +19,9 @@ void rememberViewedHandAndForgetRemovedCards() { Player target = createPlayer(game, "target", "hacan", "yellow"); Player other = createPlayer(game, "other", "saar", "black"); - target.setActionCard("direct_hit1", 11); - target.setActionCard("skilled_retreat1", 12); - target.setActionCard("skilled_retreat2", 13); + target.setActionCard("dh1", 11); + target.setActionCard("s_retreat1", 12); + target.setActionCard("s_retreat2", 13); LinkedHashMap players = new LinkedHashMap<>(); players.put(viewer.getUserID(), viewer); @@ -32,12 +32,11 @@ void rememberViewedHandAndForgetRemovedCards() { KnownActionCardsService.rememberViewedHand(viewer, target); assertThat(viewer.getStoredList("knownActionCards_hacan")) - .containsExactlyInAnyOrder("direct_hit1", "skilled_retreat1", "skilled_retreat2"); + .containsExactlyInAnyOrder("dh1", "s_retreat1", "s_retreat2"); target.removeActionCard(12); - assertThat(viewer.getStoredList("knownActionCards_hacan")) - .containsExactlyInAnyOrder("direct_hit1", "skilled_retreat2"); + assertThat(viewer.getStoredList("knownActionCards_hacan")).containsExactlyInAnyOrder("dh1", "s_retreat2"); } @Test @@ -54,14 +53,14 @@ void getKnownActionCardsTextGroupsByPlayerAndCardName() { players.put(secondTarget.getUserID(), secondTarget); game.setPlayers(players); - viewer.addToStoredList("knownActionCards_hacan", "direct_hit1", "skilled_retreat1"); - viewer.addToStoredList("knownActionCards_saar", "skilled_retreat2"); + viewer.addToStoredList("knownActionCards_hacan", "dh1", "s_retreat1"); + viewer.addToStoredList("knownActionCards_saar", "s_retreat2"); String text = KnownActionCardsService.getKnownActionCardsText(game, viewer); assertThat(text).contains("Known action cards in other players' hands"); - assertThat(text).contains("hacan"); - assertThat(text).contains("saar"); + assertThat(text).contains("Hacan"); + assertThat(text).contains("Saar"); assertThat(text).contains("Direct Hit"); assertThat(text).contains("Skilled Retreat"); } @@ -71,14 +70,17 @@ void shouldShowKnownActionCardsButtonForMageonOwnersOrTrackedKnowledge() { Game game = new Game(); Player viewer = createPlayer(game, "viewer", "yssaril", "blue"); - assertThat(KnownActionCardsService.shouldShowKnownActionCardsButton(viewer)).isFalse(); + assertThat(KnownActionCardsService.shouldShowKnownActionCardsButton(viewer)) + .isFalse(); viewer.setTechs(List.of("mi")); - assertThat(KnownActionCardsService.shouldShowKnownActionCardsButton(viewer)).isTrue(); + assertThat(KnownActionCardsService.shouldShowKnownActionCardsButton(viewer)) + .isTrue(); viewer.setTechs(List.of()); - viewer.addToStoredList("knownActionCards_hacan", "direct_hit1"); - assertThat(KnownActionCardsService.shouldShowKnownActionCardsButton(viewer)).isTrue(); + viewer.addToStoredList("knownActionCards_hacan", "dh1"); + assertThat(KnownActionCardsService.shouldShowKnownActionCardsButton(viewer)) + .isTrue(); } private Player createPlayer(Game game, String userId, String faction, String color) { From 09c1b473f04132e7187f81f377bf194b089fe37b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 07:42:42 +0000 Subject: [PATCH 3/9] Address review feedback for known action cards Agent-Logs-Url: https://github.com/AsyncTI4/TI4_map_generator_bot/sessions/15211ef4-d6b5-4c43-83ab-167767d7ae01 Co-authored-by: wholton <3317595+wholton@users.noreply.github.com> --- .../actioncard/KnownActionCardsService.java | 14 ++++++++-- .../KnownActionCardsServiceTest.java | 26 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/main/java/ti4/service/actioncard/KnownActionCardsService.java b/src/main/java/ti4/service/actioncard/KnownActionCardsService.java index 4301e2137f..fb2df0887d 100644 --- a/src/main/java/ti4/service/actioncard/KnownActionCardsService.java +++ b/src/main/java/ti4/service/actioncard/KnownActionCardsService.java @@ -78,12 +78,22 @@ public static String getKnownActionCardsText(Game game, Player viewer) { private static String getStorageKey(Player target) { String targetId = target.getFaction(); - if (targetId == null || targetId.isBlank() || "null".equalsIgnoreCase(targetId)) { + if (isUnset(targetId)) { targetId = target.getColor(); } - if (targetId == null || targetId.isBlank() || "null".equalsIgnoreCase(targetId)) { + if (isUnset(targetId)) { targetId = target.getUserID(); } + if (isUnset(targetId)) { + targetId = target.getUserName(); + } + if (isUnset(targetId)) { + targetId = "unknownPlayer"; + } return KNOWN_ACTION_CARDS_PREFIX + targetId; } + + private static boolean isUnset(String value) { + return value == null || value.isBlank() || "null".equalsIgnoreCase(value); + } } diff --git a/src/test/java/ti4/service/actioncard/KnownActionCardsServiceTest.java b/src/test/java/ti4/service/actioncard/KnownActionCardsServiceTest.java index 2acb276363..034ef3b238 100644 --- a/src/test/java/ti4/service/actioncard/KnownActionCardsServiceTest.java +++ b/src/test/java/ti4/service/actioncard/KnownActionCardsServiceTest.java @@ -65,6 +65,32 @@ void getKnownActionCardsTextGroupsByPlayerAndCardName() { assertThat(text).contains("Skilled Retreat"); } + @Test + void forgetRemovedCardsForEveryViewerTrackingThatHand() { + Game game = new Game(); + + Player firstViewer = createPlayer(game, "viewer1", "yssaril", "blue"); + Player secondViewer = createPlayer(game, "viewer2", "jolnar", "purple"); + Player target = createPlayer(game, "target", "hacan", "yellow"); + + target.setActionCard("dh1", 11); + target.setActionCard("s_retreat1", 12); + + LinkedHashMap players = new LinkedHashMap<>(); + players.put(firstViewer.getUserID(), firstViewer); + players.put(secondViewer.getUserID(), secondViewer); + players.put(target.getUserID(), target); + game.setPlayers(players); + + KnownActionCardsService.rememberViewedHand(firstViewer, target); + KnownActionCardsService.rememberViewedHand(secondViewer, target); + + target.removeActionCard(11); + + assertThat(firstViewer.getStoredList("knownActionCards_hacan")).containsExactly("s_retreat1"); + assertThat(secondViewer.getStoredList("knownActionCards_hacan")).containsExactly("s_retreat1"); + } + @Test void shouldShowKnownActionCardsButtonForMageonOwnersOrTrackedKnowledge() { Game game = new Game(); From d6ce79f8b06b2261fe694a2c9cfdb2c9bcb8245f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 02:18:04 +0000 Subject: [PATCH 4/9] Wire generic action card knowledge tracking Agent-Logs-Url: https://github.com/AsyncTI4/TI4_map_generator_bot/sessions/e69057d1-0395-4f7e-a09f-dfb6ef6c9824 Co-authored-by: wholton <3317595+wholton@users.noreply.github.com> --- .../interactions/commands/cardsac/ShowAC.java | 3 +- .../commands/cardsac/ShowACToAll.java | 3 +- .../actioncard/KnownActionCardsService.java | 21 +++++++++++++ .../KnownActionCardsServiceTest.java | 31 +++++++++++++++++++ 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/main/java/ti4/discord/interactions/commands/cardsac/ShowAC.java b/src/main/java/ti4/discord/interactions/commands/cardsac/ShowAC.java index 5e1ddb7bff..e678745b97 100644 --- a/src/main/java/ti4/discord/interactions/commands/cardsac/ShowAC.java +++ b/src/main/java/ti4/discord/interactions/commands/cardsac/ShowAC.java @@ -12,6 +12,7 @@ import ti4.helpers.Constants; import ti4.image.Mapper; import ti4.message.MessageHelper; +import ti4.service.actioncard.KnownActionCardsService; class ShowAC extends GameStateSubcommand { @@ -58,7 +59,7 @@ public void execute(SlashCommandInteractionEvent event) { return; } - ActionCardHelper.sendActionCardInfo(game, player); + KnownActionCardsService.rememberShownActionCard(playerToShowTo, player, acID); MessageHelper.sendMessageToPlayerCardsInfoThread(playerToShowTo, sb); } } diff --git a/src/main/java/ti4/discord/interactions/commands/cardsac/ShowACToAll.java b/src/main/java/ti4/discord/interactions/commands/cardsac/ShowACToAll.java index 3674c22f5a..0d99e0fe58 100644 --- a/src/main/java/ti4/discord/interactions/commands/cardsac/ShowACToAll.java +++ b/src/main/java/ti4/discord/interactions/commands/cardsac/ShowACToAll.java @@ -10,6 +10,7 @@ import ti4.helpers.Constants; import ti4.image.Mapper; import ti4.message.MessageHelper; +import ti4.service.actioncard.KnownActionCardsService; class ShowACToAll extends GameStateSubcommand { @@ -43,7 +44,7 @@ public void execute(SlashCommandInteractionEvent event) { + player.getUserName() + "\n" + "Shown Action Card:" + "\n" + Mapper.getActionCard(acID).getRepresentation(game) + "\n"; - player.setActionCard(acID); + KnownActionCardsService.rememberShownActionCardToAll(game, player, acID); MessageHelper.sendMessageToChannel(event.getChannel(), sb); } } diff --git a/src/main/java/ti4/service/actioncard/KnownActionCardsService.java b/src/main/java/ti4/service/actioncard/KnownActionCardsService.java index fb2df0887d..57c536f2a5 100644 --- a/src/main/java/ti4/service/actioncard/KnownActionCardsService.java +++ b/src/main/java/ti4/service/actioncard/KnownActionCardsService.java @@ -26,6 +26,27 @@ public static void rememberViewedHand(Player viewer, Player target) { } } + public static void rememberShownActionCard(Player viewer, Player target, String actionCardId) { + if (viewer == null || target == null || viewer == target || isUnset(actionCardId)) { + return; + } + + String storageKey = getStorageKey(target); + if (!viewer.getStoredList(storageKey).contains(actionCardId)) { + viewer.addToStoredList(storageKey, actionCardId); + } + } + + public static void rememberShownActionCardToAll(Game game, Player target, String actionCardId) { + if (game == null || target == null || isUnset(actionCardId)) { + return; + } + + for (Player player : game.getRealPlayers()) { + rememberShownActionCard(player, target, actionCardId); + } + } + public static void forgetCardFromKnownHands(Game game, Player target, String actionCardId) { if (game == null || target == null || actionCardId == null || actionCardId.isBlank()) { return; diff --git a/src/test/java/ti4/service/actioncard/KnownActionCardsServiceTest.java b/src/test/java/ti4/service/actioncard/KnownActionCardsServiceTest.java index 034ef3b238..c9d1de9363 100644 --- a/src/test/java/ti4/service/actioncard/KnownActionCardsServiceTest.java +++ b/src/test/java/ti4/service/actioncard/KnownActionCardsServiceTest.java @@ -109,6 +109,37 @@ void shouldShowKnownActionCardsButtonForMageonOwnersOrTrackedKnowledge() { .isTrue(); } + @Test + void rememberShownActionCardTracksSingleReveal() { + Game game = new Game(); + Player viewer = createPlayer(game, "viewer", "sol", "blue"); + Player target = createPlayer(game, "target", "hacan", "yellow"); + + KnownActionCardsService.rememberShownActionCard(viewer, target, "dh1"); + + assertThat(viewer.getStoredList("knownActionCards_hacan")).containsExactly("dh1"); + } + + @Test + void rememberShownActionCardToAllTracksPublicRevealForEveryoneElse() { + Game game = new Game(); + Player source = createPlayer(game, "source", "sol", "blue"); + Player viewerOne = createPlayer(game, "viewer1", "hacan", "yellow"); + Player viewerTwo = createPlayer(game, "viewer2", "saar", "black"); + + LinkedHashMap players = new LinkedHashMap<>(); + players.put(source.getUserID(), source); + players.put(viewerOne.getUserID(), viewerOne); + players.put(viewerTwo.getUserID(), viewerTwo); + game.setPlayers(players); + + KnownActionCardsService.rememberShownActionCardToAll(game, source, "dh1"); + + assertThat(source.getStoredList("knownActionCards_sol")).isEmpty(); + assertThat(viewerOne.getStoredList("knownActionCards_sol")).containsExactly("dh1"); + assertThat(viewerTwo.getStoredList("knownActionCards_sol")).containsExactly("dh1"); + } + private Player createPlayer(Game game, String userId, String faction, String color) { Player player = new Player(userId, userId, game); player.setFaction(faction); From aec789e20c5105fb3bf822276725d224b535c2ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 02:23:43 +0000 Subject: [PATCH 5/9] Finalize generic action card knowledge tracking Agent-Logs-Url: https://github.com/AsyncTI4/TI4_map_generator_bot/sessions/e69057d1-0395-4f7e-a09f-dfb6ef6c9824 Co-authored-by: wholton <3317595+wholton@users.noreply.github.com> --- .../java/ti4/discord/interactions/commands/cardsac/ShowAC.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/ti4/discord/interactions/commands/cardsac/ShowAC.java b/src/main/java/ti4/discord/interactions/commands/cardsac/ShowAC.java index e678745b97..9058d412eb 100644 --- a/src/main/java/ti4/discord/interactions/commands/cardsac/ShowAC.java +++ b/src/main/java/ti4/discord/interactions/commands/cardsac/ShowAC.java @@ -8,7 +8,6 @@ import ti4.discord.interactions.commands.GameStateSubcommand; import ti4.game.Game; import ti4.game.Player; -import ti4.helpers.ActionCardHelper; import ti4.helpers.Constants; import ti4.image.Mapper; import ti4.message.MessageHelper; From 0db3e23cb5e1261eb22ee5bf1b3f7aaf869ba0ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 02:29:23 +0000 Subject: [PATCH 6/9] Stabilize known action card storage keys Agent-Logs-Url: https://github.com/AsyncTI4/TI4_map_generator_bot/sessions/e69057d1-0395-4f7e-a09f-dfb6ef6c9824 Co-authored-by: wholton <3317595+wholton@users.noreply.github.com> --- .../actioncard/KnownActionCardsService.java | 41 +++++++++++++++---- .../KnownActionCardsServiceTest.java | 20 ++++----- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/main/java/ti4/service/actioncard/KnownActionCardsService.java b/src/main/java/ti4/service/actioncard/KnownActionCardsService.java index 57c536f2a5..cbd54d7752 100644 --- a/src/main/java/ti4/service/actioncard/KnownActionCardsService.java +++ b/src/main/java/ti4/service/actioncard/KnownActionCardsService.java @@ -1,7 +1,9 @@ package ti4.service.actioncard; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import lombok.experimental.UtilityClass; import ti4.game.Game; import ti4.game.Player; @@ -18,11 +20,10 @@ public static void rememberViewedHand(Player viewer, Player target) { return; } - String storageKey = getStorageKey(target); - viewer.removeStoredValue(storageKey); + removeTrackedKnowledge(viewer, target); List knownCards = new ArrayList<>(target.getActionCards().keySet()); if (!knownCards.isEmpty()) { - viewer.addToStoredList(storageKey, knownCards.toArray(String[]::new)); + viewer.addToStoredList(getStorageKey(target), knownCards.toArray(String[]::new)); } } @@ -54,10 +55,14 @@ public static void forgetCardFromKnownHands(Game game, Player target, String act String storageKey = getStorageKey(target); for (Player player : game.getPlayers().values()) { - if (player == target || !player.hasStoredValue(storageKey)) { + if (player == target) { continue; } - player.removeFromStoredList(storageKey, actionCardId); + for (String key : getStorageKeys(target)) { + if (player.hasStoredValue(key)) { + player.removeFromStoredList(key, actionCardId); + } + } } } @@ -79,7 +84,7 @@ public static String getKnownActionCardsText(Game game, Player viewer) { continue; } - List knownCards = viewer.getStoredList(getStorageKey(target)); + List knownCards = getKnownCards(viewer, target); if (knownCards.isEmpty()) { continue; } @@ -98,6 +103,13 @@ public static String getKnownActionCardsText(Game game, Player viewer) { } private static String getStorageKey(Player target) { + return KNOWN_ACTION_CARDS_PREFIX + target.getUserID(); + } + + private static List getStorageKeys(Player target) { + Set keys = new LinkedHashSet<>(); + keys.add(getStorageKey(target)); + String targetId = target.getFaction(); if (isUnset(targetId)) { targetId = target.getColor(); @@ -111,7 +123,22 @@ private static String getStorageKey(Player target) { if (isUnset(targetId)) { targetId = "unknownPlayer"; } - return KNOWN_ACTION_CARDS_PREFIX + targetId; + keys.add(KNOWN_ACTION_CARDS_PREFIX + targetId); + return new ArrayList<>(keys); + } + + private static List getKnownCards(Player viewer, Player target) { + Set knownCards = new LinkedHashSet<>(); + for (String key : getStorageKeys(target)) { + knownCards.addAll(viewer.getStoredList(key)); + } + return new ArrayList<>(knownCards); + } + + private static void removeTrackedKnowledge(Player viewer, Player target) { + for (String key : getStorageKeys(target)) { + viewer.removeStoredValue(key); + } } private static boolean isUnset(String value) { diff --git a/src/test/java/ti4/service/actioncard/KnownActionCardsServiceTest.java b/src/test/java/ti4/service/actioncard/KnownActionCardsServiceTest.java index c9d1de9363..8f589f827e 100644 --- a/src/test/java/ti4/service/actioncard/KnownActionCardsServiceTest.java +++ b/src/test/java/ti4/service/actioncard/KnownActionCardsServiceTest.java @@ -31,12 +31,12 @@ void rememberViewedHandAndForgetRemovedCards() { KnownActionCardsService.rememberViewedHand(viewer, target); - assertThat(viewer.getStoredList("knownActionCards_hacan")) + assertThat(viewer.getStoredList("knownActionCards_target")) .containsExactlyInAnyOrder("dh1", "s_retreat1", "s_retreat2"); target.removeActionCard(12); - assertThat(viewer.getStoredList("knownActionCards_hacan")).containsExactlyInAnyOrder("dh1", "s_retreat2"); + assertThat(viewer.getStoredList("knownActionCards_target")).containsExactlyInAnyOrder("dh1", "s_retreat2"); } @Test @@ -53,8 +53,8 @@ void getKnownActionCardsTextGroupsByPlayerAndCardName() { players.put(secondTarget.getUserID(), secondTarget); game.setPlayers(players); - viewer.addToStoredList("knownActionCards_hacan", "dh1", "s_retreat1"); - viewer.addToStoredList("knownActionCards_saar", "s_retreat2"); + viewer.addToStoredList("knownActionCards_first", "dh1", "s_retreat1"); + viewer.addToStoredList("knownActionCards_second", "s_retreat2"); String text = KnownActionCardsService.getKnownActionCardsText(game, viewer); @@ -87,8 +87,8 @@ void forgetRemovedCardsForEveryViewerTrackingThatHand() { target.removeActionCard(11); - assertThat(firstViewer.getStoredList("knownActionCards_hacan")).containsExactly("s_retreat1"); - assertThat(secondViewer.getStoredList("knownActionCards_hacan")).containsExactly("s_retreat1"); + assertThat(firstViewer.getStoredList("knownActionCards_target")).containsExactly("s_retreat1"); + assertThat(secondViewer.getStoredList("knownActionCards_target")).containsExactly("s_retreat1"); } @Test @@ -117,7 +117,7 @@ void rememberShownActionCardTracksSingleReveal() { KnownActionCardsService.rememberShownActionCard(viewer, target, "dh1"); - assertThat(viewer.getStoredList("knownActionCards_hacan")).containsExactly("dh1"); + assertThat(viewer.getStoredList("knownActionCards_target")).containsExactly("dh1"); } @Test @@ -135,9 +135,9 @@ void rememberShownActionCardToAllTracksPublicRevealForEveryoneElse() { KnownActionCardsService.rememberShownActionCardToAll(game, source, "dh1"); - assertThat(source.getStoredList("knownActionCards_sol")).isEmpty(); - assertThat(viewerOne.getStoredList("knownActionCards_sol")).containsExactly("dh1"); - assertThat(viewerTwo.getStoredList("knownActionCards_sol")).containsExactly("dh1"); + assertThat(source.getStoredList("knownActionCards_source")).isEmpty(); + assertThat(viewerOne.getStoredList("knownActionCards_source")).containsExactly("dh1"); + assertThat(viewerTwo.getStoredList("knownActionCards_source")).containsExactly("dh1"); } private Player createPlayer(Game game, String userId, String faction, String color) { From 53ad95ffeac043ca87fa231f9a32fdb62ff4b892 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 02:31:05 +0000 Subject: [PATCH 7/9] Refine known action card cleanup loop Agent-Logs-Url: https://github.com/AsyncTI4/TI4_map_generator_bot/sessions/e69057d1-0395-4f7e-a09f-dfb6ef6c9824 Co-authored-by: wholton <3317595+wholton@users.noreply.github.com> --- .../java/ti4/service/actioncard/KnownActionCardsService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/ti4/service/actioncard/KnownActionCardsService.java b/src/main/java/ti4/service/actioncard/KnownActionCardsService.java index cbd54d7752..45baa1cd00 100644 --- a/src/main/java/ti4/service/actioncard/KnownActionCardsService.java +++ b/src/main/java/ti4/service/actioncard/KnownActionCardsService.java @@ -53,12 +53,12 @@ public static void forgetCardFromKnownHands(Game game, Player target, String act return; } - String storageKey = getStorageKey(target); + List storageKeys = getStorageKeys(target); for (Player player : game.getPlayers().values()) { if (player == target) { continue; } - for (String key : getStorageKeys(target)) { + for (String key : storageKeys) { if (player.hasStoredValue(key)) { player.removeFromStoredList(key, actionCardId); } From 9a90156c90a0f31d94a52605a2b1e7d6d9832f59 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 02:32:47 +0000 Subject: [PATCH 8/9] Clarify legacy known card migration Agent-Logs-Url: https://github.com/AsyncTI4/TI4_map_generator_bot/sessions/e69057d1-0395-4f7e-a09f-dfb6ef6c9824 Co-authored-by: wholton <3317595+wholton@users.noreply.github.com> --- .../ti4/service/actioncard/KnownActionCardsService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/ti4/service/actioncard/KnownActionCardsService.java b/src/main/java/ti4/service/actioncard/KnownActionCardsService.java index 45baa1cd00..6774f4fce4 100644 --- a/src/main/java/ti4/service/actioncard/KnownActionCardsService.java +++ b/src/main/java/ti4/service/actioncard/KnownActionCardsService.java @@ -59,9 +59,7 @@ public static void forgetCardFromKnownHands(Game game, Player target, String act continue; } for (String key : storageKeys) { - if (player.hasStoredValue(key)) { - player.removeFromStoredList(key, actionCardId); - } + player.removeFromStoredList(key, actionCardId); } } } @@ -110,6 +108,8 @@ private static List getStorageKeys(Player target) { Set keys = new LinkedHashSet<>(); keys.add(getStorageKey(target)); + // Keep reading/removing the older non-userID keys so tracked knowledge survives migration + // from the earlier faction/color-based storage format. String targetId = target.getFaction(); if (isUnset(targetId)) { targetId = target.getColor(); From f0b9a6a37ec5bf726d8628340e11ff9152ca1272 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 02:34:22 +0000 Subject: [PATCH 9/9] Guard known card storage key fallback Agent-Logs-Url: https://github.com/AsyncTI4/TI4_map_generator_bot/sessions/e69057d1-0395-4f7e-a09f-dfb6ef6c9824 Co-authored-by: wholton <3317595+wholton@users.noreply.github.com> --- .../ti4/service/actioncard/KnownActionCardsService.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/ti4/service/actioncard/KnownActionCardsService.java b/src/main/java/ti4/service/actioncard/KnownActionCardsService.java index 6774f4fce4..bff17943c4 100644 --- a/src/main/java/ti4/service/actioncard/KnownActionCardsService.java +++ b/src/main/java/ti4/service/actioncard/KnownActionCardsService.java @@ -101,7 +101,14 @@ public static String getKnownActionCardsText(Game game, Player viewer) { } private static String getStorageKey(Player target) { - return KNOWN_ACTION_CARDS_PREFIX + target.getUserID(); + String targetId = target.getUserID(); + if (isUnset(targetId)) { + targetId = target.getUserName(); + } + if (isUnset(targetId)) { + targetId = "unknownPlayer"; + } + return KNOWN_ACTION_CARDS_PREFIX + targetId; } private static List getStorageKeys(Player target) {