diff --git a/src/main/java/net/tfminecraft/games/game/PokerGame.java b/src/main/java/net/tfminecraft/games/game/PokerGame.java index 8c285ce..d50beb0 100644 --- a/src/main/java/net/tfminecraft/games/game/PokerGame.java +++ b/src/main/java/net/tfminecraft/games/game/PokerGame.java @@ -210,7 +210,6 @@ public void onPlayWord(Table table, Player player, String word) { street.acted.clear(); } street.capped.add(id); - announceAction(table, player, "allin"); } case "fold" -> { street.folded.add(id); @@ -221,6 +220,9 @@ public void onPlayWord(Table table, Player player, String word) { return; } } + if (street.capped.contains(id)) { + announceAllIn(table, player); + } street.acted.add(id); finishOrAdvance(table, new ArrayList<>(table.actives()), true); } @@ -284,10 +286,17 @@ public String extraLabel(Table table) { } if (table.live()) { lines.add(streetLabel(table.phase())); + Street street = streets.get(table.getId()); + if (street != null) { + for (UUID id : table.actives()) { + if (street.capped.contains(id) && !street.folded.contains(id)) { + lines.add(Messages.get("label.holdem_allin", "name", RpNames.of(id))); + } + } + } UUID actor = table.actor(); if (actor != null) { lines.add(Messages.get("label.turn", "name", RpNames.of(actor))); - Street street = streets.get(table.getId()); int toCall = street == null ? 0 : Math.max(0, street.currentBet - streetContrib(table, actor)); lines.add(Messages.get("label.holdem_tocall", "n", String.valueOf(toCall))); } @@ -801,7 +810,10 @@ private boolean postForcedBets(Table table, Street street) { if (small > big) street.currentBet = small; if (small > 0 || big > 0 || tournament.enabled()) { for (Player player : seatedOnline(table)) { - if (isAllIn(table, player)) street.capped.add(player.getUniqueId()); + if (isAllIn(table, player)) { + street.capped.add(player.getUniqueId()); + announceAllIn(table, player); + } } } return true; @@ -833,6 +845,14 @@ private void leaveTournament(Table table, Player player) { finishOrAdvance(table, before, false); } + private static void announceAllIn(Table table, Player player) { + announceAction(table, player, "allin"); + player.sendMessage(Messages.get("poker.allin_notice")); + if (!table.poker().enabled()) { + player.sendMessage(Messages.get("poker.cash_allin_hint")); + } + } + private static void announceAction(Table table, Player player, String action) { tellSeated(table, Messages.get("poker.action_" + action, "name", RpNames.of(player.getUniqueId()))); } diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index 3855719..9fbe7d7 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -110,6 +110,8 @@ poker: action_raised: "#55ff55{name} raises to {n}." action_folded: "#aaaaaa{name} folds." action_allin: "#ffaa00{name} goes all in." + allin_notice: "#ffaa00You are all in. You stay in the hand and have no more betting turns." + cash_allin_hint: "#aaaaaaNo spendable coins remain in your inventory. Use /deco toitem before the next hand to take coins from your pouch." stake_closed: "#ff5555Stake only on your turn in this hand. Tournament chips use /games poker bet ." blind_failed: "#ff5555Cannot collect blinds exactly. Bring suitable coins before dealing." prize_pending: "#ff5555Some prize coins remain on the table. Retry finishing before starting another tournament." @@ -260,6 +262,7 @@ label: holdem_river: "#aaaaaaRiver" holdem_showdown: "#aaaaaaShowdown" holdem_tocall: "#aaaaaaTo call {n}" + holdem_allin: "#ffaa00{name}: All in" draw_bet: "#aaaaaaBetting" draw_draw: "#aaaaaaDraw" draw_showdown: "#aaaaaaShowdown" diff --git a/src/test/java/net/tfminecraft/games/MessagesTest.java b/src/test/java/net/tfminecraft/games/MessagesTest.java index e6c5423..d1c5b04 100644 --- a/src/test/java/net/tfminecraft/games/MessagesTest.java +++ b/src/test/java/net/tfminecraft/games/MessagesTest.java @@ -76,6 +76,9 @@ void bundledNewMessagesFillMissingKeysWithoutReplacingCustomText() throws Except assertEquals("Custom Alice", Messages.get("poker.action_called", "name", "Alice")); assertTrue(Messages.getRaw("poker.action_allin").contains("{name}")); assertFalse(Messages.getRaw("poker.action_allin").equals("poker.action_allin")); + assertTrue(Messages.getRaw("poker.allin_notice").contains("betting turns")); + assertTrue(Messages.getRaw("poker.cash_allin_hint").contains("/deco toitem")); + assertTrue(Messages.getRaw("label.holdem_allin").contains("{name}")); } @Test diff --git a/src/test/java/net/tfminecraft/games/game/PokerGameTest.java b/src/test/java/net/tfminecraft/games/game/PokerGameTest.java index 98eee51..0e2a84f 100644 --- a/src/test/java/net/tfminecraft/games/game/PokerGameTest.java +++ b/src/test/java/net/tfminecraft/games/game/PokerGameTest.java @@ -536,6 +536,60 @@ void chipsPastTheBetAreARaiseEvenWhenCalledACheckOrCall() { assertEquals(bob.getUniqueId(), table.actor(), "the first bettor has to answer the raise"); } + @Test + void cashAllInExplainsSkippedTurnsAndStaysLabelledForTheRestOfTheHand() { + start(alice, bob); + contribute(bob, 5); + broke.add(bob.getUniqueId()); + act("raise"); + assertTrue(game.extraLabel(table).lines().anyMatch(line -> + line.startsWith("label.holdem_allin") && line.contains(bob.getName()))); + assertEquals(alice.getUniqueId(), table.actor()); + contribute(alice, 5); + act("call"); + drain(); + for (String phase : List.of(PokerGame.FLOP, PokerGame.TURN, PokerGame.RIVER)) { + assertEquals(phase, table.phase()); + assertEquals(alice.getUniqueId(), table.actor()); + assertTrue(game.extraLabel(table).contains("label.holdem_allin")); + act("check"); + drain(); + } + assertFalse(table.live()); + assertFalse(game.extraLabel(table).contains("label.holdem_allin")); + List heard = new ArrayList<>(); + for (String line = ((PlayerMock) bob).nextMessage(); line != null; line = ((PlayerMock) bob).nextMessage()) { + heard.add(line); + } + assertEquals(1, heard.stream().filter(line -> line.startsWith("poker.action_allin")).count()); + assertEquals(1, heard.stream().filter(line -> line.equals("poker.allin_notice")).count()); + assertEquals(1, heard.stream().filter(line -> line.equals("poker.cash_allin_hint")).count()); + for (String line = ((PlayerMock) alice).nextMessage(); line != null; line = ((PlayerMock) alice).nextMessage()) { + assertNotEquals("poker.allin_notice", line, "only the all-in player receives the personal notice"); + assertNotEquals("poker.cash_allin_hint", line); + } + } + + @Test + void explicitTournamentAllInIsAnnouncedOnceWithoutCashAdvice() { + table.poker().configure(10, 10, 0, 0, 0); + table.poker().buy(alice.getUniqueId()); + table.poker().buy(bob.getUniqueId()); + start(alice, bob); + act("allin"); + assertEquals(0, table.poker().stack(bob.getUniqueId())); + assertEquals(alice.getUniqueId(), table.actor()); + assertTrue(game.extraLabel(table).lines().anyMatch(line -> + line.startsWith("label.holdem_allin") && line.contains(bob.getName()))); + List heard = new ArrayList<>(); + for (String line = ((PlayerMock) bob).nextMessage(); line != null; line = ((PlayerMock) bob).nextMessage()) { + heard.add(line); + } + assertEquals(1, heard.stream().filter(line -> line.startsWith("poker.action_allin")).count()); + assertEquals(1, heard.stream().filter(line -> line.equals("poker.allin_notice")).count()); + assertFalse(heard.contains("poker.cash_allin_hint")); + } + @Test void allInSeatIsSkippedWhileTheOthersKeepRaising() { start(alice, bob, carol); diff --git a/src/test/java/net/tfminecraft/games/table/TableManagerGameRoundTest.java b/src/test/java/net/tfminecraft/games/table/TableManagerGameRoundTest.java index eb62933..6afb951 100644 --- a/src/test/java/net/tfminecraft/games/table/TableManagerGameRoundTest.java +++ b/src/test/java/net/tfminecraft/games/table/TableManagerGameRoundTest.java @@ -4,13 +4,18 @@ import static org.mockito.Mockito.*; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.UUID; import org.bukkit.Material; import org.bukkit.entity.Entity; +import org.bukkit.event.player.AsyncPlayerChatEvent; import org.bukkit.event.player.PlayerInteractAtEntityEvent; import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; import org.bukkit.util.Vector; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -26,8 +31,9 @@ /** A complete poker or Five-Draw round across the real game, table, deck and money implementations. */ class TableManagerGameRoundTest extends TableManagerFixture { @ParameterizedTest - @CsvSource({"poker, 2", "draw, 5"}) - void shoeToShowdownConservesCardsAndMoneyAcrossRealComponents(String gameId, int cardsPerPlayer) { + @CsvSource({"poker, 2, 0, 4, 4", "draw, 5, 0, 0, 0", "poker, 2, 1, 4, 4", "poker, 2, 1, 4, 0"}) + void shoeToShowdownConservesCardsAndMoneyAcrossRealComponents(String gameId, int cardsPerPlayer, + int blind, int pocketCoins, int otherPocketCoins) { List fullDeck = new ArrayList<>(); for (String suit : List.of("oseni", "cerrith", "clubs", "hearts")) { for (int rank = 1; rank <= 13; rank++) { @@ -49,6 +55,10 @@ void shoeToShowdownConservesCardsAndMoneyAcrossRealComponents(String gameId, int assertEquals(player.getUniqueId(), table.dealerId()); stakeCoin(other, table); assertEquals(2, table.ledger().total()); + table.setSmallBlind(blind); + table.setBigBlind(blind); + if (pocketCoins > 0) player.getInventory().setItemInMainHand(new ItemStack(Material.GOLD_NUGGET, pocketCoins)); + if (otherPocketCoins > 0) other.getInventory().setItemInMainHand(new ItemStack(Material.GOLD_NUGGET, otherPocketCoins)); assertFalse(table.live()); Entity shoe = mock(Entity.class); anchors.when(() -> WorldAnchors.tableId(shoe)).thenReturn(table.getId().toString()); @@ -60,14 +70,23 @@ void shoeToShowdownConservesCardsAndMoneyAcrossRealComponents(String gameId, int assertEquals(cardsPerPlayer, table.handOf(player.getUniqueId()).size()); assertEquals(cardsPerPlayer, table.handOf(other.getUniqueId()).size()); Set phases = new HashSet<>(); + Map> actors = new HashMap<>(); for (int action = 0; table.live() && action < 12; action++) { phases.add(table.phase()); assertNotNull(table.actor(), "A finished deal must nominate the next actor"); var actor = org.bukkit.Bukkit.getPlayer(table.actor()); assertNotNull(actor); + actors.computeIfAbsent(table.phase(), ignored -> new HashSet<>()).add(actor.getUniqueId()); assertTrue(rules.allowPlayChat(table, actor)); assertTrue(rules.extraLabel(table).contains("label.turn")); - manager.applyPlayCall(actor, "draw".equals(table.phase()) ? "draw" : "check"); + if (blind > 0 && otherPocketCoins == 0) { + assertTrue(rules.extraLabel(table).contains("label.holdem_allin"), + "the player skipped after posting their last coin must stay visibly all in"); + } + AsyncPlayerChatEvent chat = new AsyncPlayerChatEvent(true, actor, + "draw".equals(table.phase()) ? "draw" : "check", Set.of(player, other)); + manager.onPlayChat(chat); + assertTrue(chat.isCancelled(), "The acting seat's play word must reach the game"); tick(40); } assertFalse(table.live(), "Checking/standing pat through the whole round must settle"); @@ -77,10 +96,23 @@ void shoeToShowdownConservesCardsAndMoneyAcrossRealComponents(String gameId, int assertEquals(52, table.getDeck().remaining() + table.getDeck().discarded()); assertEquals(52, java.util.stream.Stream.concat(table.getDeck().remainingIds().stream(), table.getDeck().discardedIds().stream()).distinct().count()); - assertEquals(2, java.util.stream.Stream.of(player, other) + assertEquals(2 + pocketCoins + otherPocketCoins, java.util.stream.Stream.of(player, other) .flatMap(p -> p.getInventory().all(Material.GOLD_NUGGET).values().stream()) .mapToInt(item -> item.getAmount()).sum()); assertEquals(other.getUniqueId(), table.dealerId(), "The button advances after settlement"); assertEquals(gameId.equals("poker") ? Set.of("preflop", "flop", "turn", "river") : Set.of("bet", "draw"), phases); + Set expectedActors = blind > 0 && otherPocketCoins == 0 + ? Set.of(player.getUniqueId()) : Set.of(player.getUniqueId(), other.getUniqueId()); + for (String phase : phases) { + assertEquals(expectedActors, actors.get(phase), "Only all-in seats skip betting in " + phase); + } + List heard = new ArrayList<>(); + for (String line = other.nextMessage(); line != null; line = other.nextMessage()) { + heard.add(line); + } + int notices = blind > 0 && otherPocketCoins == 0 ? 1 : 0; + assertEquals(notices, heard.stream().filter("poker.action_allin"::equals).count()); + assertEquals(notices, heard.stream().filter("poker.allin_notice"::equals).count()); + assertEquals(notices, heard.stream().filter("poker.cash_allin_hint"::equals).count()); } }