Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/main/java/net/tfminecraft/games/game/DrawGame.java
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ public void onPlayWord(Table table, Player player, String word) {
return;
}
int contrib = streetContrib(table, id);
if (contrib > street.currentBet && ("check".equals(word) || "call".equals(word))) {
// Chips past the bet are a bet whatever it is called, so the others have to answer it.
word = "raise";
}
switch (word) {
case "check" -> {
if (contrib < street.currentBet) {
Expand All @@ -147,6 +151,12 @@ public void onPlayWord(Table table, Player player, String word) {
}
case "call" -> {
if (contrib < street.currentBet) {
// Calling short is only an all in: chips still in pockets have to go down first.
if (!WagerEngine.get().allIn(table, player)) {
player.sendMessage(Messages.get("draw.need_call",
"n", String.valueOf(street.currentBet - contrib)));
return;
}
street.capped.add(id);
} else {
street.capped.remove(id);
Expand Down Expand Up @@ -195,6 +205,17 @@ public void onReturnedSelected(Table table, Player player, int count) {
});
}

/** A live hand takes money only from seats still in it; anyone else waits for the next one. */
@Override
public boolean allowStake(Table table, Player player) {
if (!table.live()) {
return true;
}
UUID id = player.getUniqueId();
Street street = streets.get(table.getId());
return table.actives().contains(id) && (street == null || !street.folded.contains(id));
}

@Override
public void onFeltPilesChanged(Table table) {
TableManager.get().refreshLabel(table);
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/net/tfminecraft/games/game/Game.java
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@ default void onDealerGone(Table table) {
onTableReady(table);
}

/**
* Whether this player may put money on the felt right now. Games that deal a hand to a fixed
* set of seats say no to anyone not in the live hand, so a late stake cannot buy into a hand
* already dealt and a folded seat cannot feed a pot it has given up.
*/
default boolean allowStake(Table table, Player player) {
return true;
}

/** Every game reacts to chips arriving or a seat leaving, if only to refresh its label. */
void onChipIn(Table table, Player player);

Expand Down
21 changes: 21 additions & 0 deletions src/main/java/net/tfminecraft/games/game/PokerGame.java
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ public void onPlayWord(Table table, Player player, String word) {
Street street = streets.get(table.getId());
UUID id = player.getUniqueId();
int contrib = streetContrib(table, id);
if (contrib > street.currentBet && ("check".equals(word) || "call".equals(word))) {
// Chips past the bet are a bet whatever it is called, so the others have to answer it.
word = "raise";
}
switch (word) {
case "check" -> {
if (contrib < street.currentBet) {
Expand All @@ -133,6 +137,12 @@ public void onPlayWord(Table table, Player player, String word) {
}
case "call" -> {
if (contrib < street.currentBet) {
// Calling short is only an all in: chips still in pockets have to go down first.
if (!WagerEngine.get().allIn(table, player)) {
player.sendMessage(Messages.get("poker.need_call",
"n", String.valueOf(street.currentBet - contrib)));
return;
}
street.capped.add(id);
} else {
street.capped.remove(id);
Expand Down Expand Up @@ -162,6 +172,17 @@ public void onPlayWord(Table table, Player player, String word) {
finishOrAdvance(table, new ArrayList<>(table.actives()), true);
}

/** A live hand takes money only from seats still in it; anyone else waits for the next one. */
@Override
public boolean allowStake(Table table, Player player) {
if (!table.live()) {
return true;
}
UUID id = player.getUniqueId();
Street street = streets.get(table.getId());
return table.actives().contains(id) && (street == null || !street.folded.contains(id));
}

@Override
public void onFeltPilesChanged(Table table) {
TableManager.get().refreshLabel(table);
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/net/tfminecraft/games/table/TableManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -2519,6 +2519,9 @@ private boolean dumpLoot(Player player, Table table, ItemStack snapshot, int den
if (dealerTray && refuseTrayStock(table, player)) {
return false;
}
if (!dealerTray && refuseStake(table, player)) {
return false;
}
int placeDenars = denars * need;
ItemStack one = snapshot.clone();
one.setAmount(1);
Expand Down Expand Up @@ -2660,6 +2663,9 @@ private boolean tryPlaceChip(Player player, Location click) {
if (!dealerTray && refuseBlackjackPlace(player, table, player.getUniqueId(), denars)) {
return true;
}
if (!dealerTray && refuseStake(table, player)) {
return true;
}
UUID bucket = dealerTray ? table.getId() : player.getUniqueId();
// One coin of exactly this kind, picked out and staked as a single movement. The coin is in
// the player's hand and worth exactly this much, so the movement always goes through.
Expand All @@ -2679,6 +2685,16 @@ private boolean tryPlaceChip(Player player, Location click) {
return true;
}

/** True when the game will not take this player's money now, such as mid-hand from a stranger. */
private static boolean refuseStake(Table table, Player player) {
Game game = gameOf(table);
if (game == null || game.allowStake(table, player)) {
return false;
}
player.sendMessage(Messages.get("wager.not_in_hand"));
return true;
}

/**
* True when this dealer must not stock the tray by hand. On a backed table the house money is
* the bank's, so personal coins going in would be money the guild then banks as its own.
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/net/tfminecraft/games/wager/WagerEngine.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ public int owned(Table table, UUID owner, int street) {
return table.ledger().total(owner, street);
}

/**
* True when this player's pockets hold nothing a table would take as a bet, so a call short
* of the current bet is an all in rather than a free ride.
*/
public boolean allIn(Table table, Player player) {
return Accounts.pockets(table, player).available() < 1;
}

public int tray(Table table) {
return table.ledger().total(table.getId());
}
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/messages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ hand:
poker:
need_players: "#ff5555Need two seated players to deal."
cannot_check: "#ff5555You cannot check. Call, raise, or fold."
need_call: "#ff5555Put {n} more on this street to call, or fold."
need_chips: "#ff5555Put chips on this street first."
checked: "#aaaaaaChecked."
called: "#aaaaaaCalled."
Expand All @@ -122,6 +123,7 @@ poker:
draw:
need_players: "#ff5555Need two seated players to deal."
cannot_check: "#ff5555You cannot check. Call, raise, or fold."
need_call: "#ff5555Put {n} more on this street to call, or fold."
need_chips: "#ff5555Put chips on this street first."
checked: "#aaaaaaChecked."
called: "#aaaaaaCalled."
Expand All @@ -148,6 +150,7 @@ wager:
need_item: "#ff5555Hold the item you want to wager."
use_click: "#ff5555Place coins and valued chips by right-clicking the table."
coins_only: "#ff5555This table only takes coins."
not_in_hand: "#ff5555You are not in this hand. Wait for the next one."
need_amount: "#ff5555Enter a whole denar amount of 1 or more."
no_vote: "#ff5555There is no wager to vote on."
not_eligible: "#ff5555You cannot vote on this wager."
Expand Down
57 changes: 57 additions & 0 deletions src/test/java/net/tfminecraft/games/game/DrawGameTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.junit.jupiter.api.BeforeEach;
import org.mockbukkit.mockbukkit.MockBukkit;
import org.mockbukkit.mockbukkit.entity.PlayerMock;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -263,6 +264,7 @@ void bettingRejectsEmptyRaiseAndUnmatchedCheckButAllowsShortAllInCall() {
act("check");
assertEquals(carol.getUniqueId(), table.actor());
contribute(carol, 3);
broke.add(carol.getUniqueId());
act("call");
contribute(alice, 20);
act("raise");
Expand Down Expand Up @@ -692,12 +694,67 @@ void removingTheTableDuringTheInitialDealStopsDealing() {
assertNull(table.actor());
}

@Test
void shortCallIsRefusedWhileChipsRemainInPocketAndCapsOnlyOnceAllIn() {
start(alice, bob, carol);
contribute(bob, 10);
act("raise");
assertEquals(carol.getUniqueId(), table.actor());
act("call");
assertEquals(carol.getUniqueId(), table.actor(), "a free call must not pass the turn");
assertTrue(((PlayerMock) carol).nextMessage().startsWith("draw.need_call"));
assertTrue(game.extraLabel(table).contains("n, 10"));
contribute(carol, 3);
act("call");
assertEquals(carol.getUniqueId(), table.actor());
assertTrue(((PlayerMock) carol).nextMessage().contains("n, 7"));
broke.add(carol.getUniqueId());
act("call");
assertEquals(alice.getUniqueId(), table.actor());
assertEquals("draw.called", ((PlayerMock) carol).nextMessage());
}

@Test
void onlySeatsStillInALiveHandMayStake() {
Player stranger = MockBukkit.getMock().addPlayer();
assertTrue(game.allowStake(table, stranger), "an idle table takes anyone's chips");
seat(alice, bob, carol);
table.startSession();
game.onSessionStart(table);
assertTrue(game.allowStake(table, alice), "seats may stake while holes are dealt");
assertFalse(game.allowStake(table, stranger), "a stranger cannot buy into a dealt hand");
drain();
assertEquals(bob.getUniqueId(), table.actor());
assertTrue(game.allowStake(table, bob));
act("fold");
assertTrue(table.live());
assertFalse(game.allowStake(table, bob), "a folded seat cannot feed the pot");
assertTrue(game.allowStake(table, carol));
assertFalse(game.allowStake(table, stranger));
}

@Test
void chipsPastTheBetAreARaiseEvenWhenCalledACheckOrCall() {
start(alice, bob, carol);
contribute(bob, 10);
act("check");
assertEquals("draw.raised[draw.raised, n, 10]", ((PlayerMock) bob).nextMessage());
assertEquals(carol.getUniqueId(), table.actor());
contribute(carol, 25);
act("call");
assertEquals("draw.raised[draw.raised, n, 25]", ((PlayerMock) carol).nextMessage());
contribute(alice, 25);
act("call");
assertEquals(bob.getUniqueId(), table.actor(), "the first bettor has to answer the raise");
}

@Test
void allInSeatIsSkippedWhileTheOthersKeepRaising() {
start(alice, bob, carol);
contribute(bob, 10);
act("raise");
contribute(carol, 3);
broke.add(carol.getUniqueId());
act("call");
contribute(alice, 20);
act("raise");
Expand Down
60 changes: 60 additions & 0 deletions src/test/java/net/tfminecraft/games/game/PokerGameTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ void raiseRequiresChipsAndReopensActionWhileShortCallCapsPlayer() {
act("check");
assertEquals(carol.getUniqueId(), table.actor());
contribute(carol, 5);
broke.add(carol.getUniqueId());
act("call");
assertEquals(alice.getUniqueId(), table.actor());
contribute(alice, 30);
Expand Down Expand Up @@ -478,12 +479,67 @@ void endingTheSessionDuringTheHoleDealStopsDealing() {
assertFalse(table.live());
}

@Test
void shortCallIsRefusedWhileChipsRemainInPocketAndCapsOnlyOnceAllIn() {
start(alice, bob, carol);
contribute(bob, 10);
act("raise");
assertEquals(carol.getUniqueId(), table.actor());
act("call");
assertEquals(carol.getUniqueId(), table.actor(), "a free call must not pass the turn");
assertTrue(((PlayerMock) carol).nextMessage().startsWith("poker.need_call"));
assertTrue(game.extraLabel(table).contains("n, 10"));
contribute(carol, 3);
act("call");
assertEquals(carol.getUniqueId(), table.actor());
assertTrue(((PlayerMock) carol).nextMessage().contains("n, 7"));
broke.add(carol.getUniqueId());
act("call");
assertEquals(alice.getUniqueId(), table.actor());
assertEquals("poker.called", ((PlayerMock) carol).nextMessage());
}

@Test
void onlySeatsStillInALiveHandMayStake() {
Player stranger = MockBukkit.getMock().addPlayer();
assertTrue(game.allowStake(table, stranger), "an idle table takes anyone's chips");
seat(alice, bob, carol);
table.startSession();
game.onSessionStart(table);
assertTrue(game.allowStake(table, alice), "seats may stake while holes are dealt");
assertFalse(game.allowStake(table, stranger), "a stranger cannot buy into a dealt hand");
drain();
assertEquals(bob.getUniqueId(), table.actor());
assertTrue(game.allowStake(table, bob));
act("fold");
assertTrue(table.live());
assertFalse(game.allowStake(table, bob), "a folded seat cannot feed the pot");
assertTrue(game.allowStake(table, carol));
assertFalse(game.allowStake(table, stranger));
}

@Test
void chipsPastTheBetAreARaiseEvenWhenCalledACheckOrCall() {
start(alice, bob, carol);
contribute(bob, 10);
act("check");
assertEquals("poker.raised[poker.raised, n, 10]", ((PlayerMock) bob).nextMessage());
assertEquals(carol.getUniqueId(), table.actor());
contribute(carol, 25);
act("call");
assertEquals("poker.raised[poker.raised, n, 25]", ((PlayerMock) carol).nextMessage());
contribute(alice, 25);
act("call");
assertEquals(bob.getUniqueId(), table.actor(), "the first bettor has to answer the raise");
}

@Test
void allInSeatIsSkippedWhileTheOthersKeepRaising() {
start(alice, bob, carol);
contribute(bob, 10);
act("raise");
contribute(carol, 3);
broke.add(carol.getUniqueId());
act("call");
assertEquals(alice.getUniqueId(), table.actor());
contribute(alice, 20);
Expand Down Expand Up @@ -680,6 +736,8 @@ abstract class GameScenarioFixture {
final Map<UUID, Integer> invested = new HashMap<>();
final Map<Integer, Map<UUID, Integer>> contributions = new HashMap<>();
final List<UUID> dealt = new ArrayList<>();
/** Seats with nothing left in their pockets to bet, so a short call is an all in. */
final java.util.Set<UUID> broke = new java.util.HashSet<>();
private MockedStatic<TableManager> managers;
private MockedStatic<WagerEngine> wagers;
private MockedStatic<Messages> messages;
Expand All @@ -704,6 +762,8 @@ void setUpScenario() {
carol = MockBukkit.getMock().addPlayer();
when(manager.table(table.getId())).thenReturn(table);
when(wager.totalsExcept(table, table.getId())).thenAnswer(call -> new HashMap<>(invested));
when(wager.allIn(eq(table), any(Player.class))).thenAnswer(call ->
broke.contains(call.getArgument(1, Player.class).getUniqueId()));
when(wager.owned(eq(table), any(UUID.class), anyInt())).thenAnswer(call ->
contributions.getOrDefault(call.getArgument(2), Map.of()).getOrDefault(call.getArgument(1), 0));
doAnswer(call -> {
Expand Down
Loading
Loading