-
Notifications
You must be signed in to change notification settings - Fork 18
/
Pot.java
322 lines (279 loc) · 13.6 KB
/
Pot.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
package org.hit.android.haim.texasholdem.common.model.game;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hit.android.haim.texasholdem.common.model.bean.game.Board;
import org.hit.android.haim.texasholdem.common.model.bean.game.Player;
import org.hit.android.haim.texasholdem.common.model.game.rank.HandRankCalculator;
import org.hit.android.haim.texasholdem.common.model.game.rank.HandRankCalculatorResult;
import org.hit.android.haim.texasholdem.common.util.Pair;
import java.util.*;
import java.util.stream.Collectors;
/**
* This class responsible for bets.<br/>
* We count all of the bets and keep them in this class, also maintaining side pots when there is a player that
* went all-in, but other players want to keep raising more than the all-in of another player.<br/>
* In this scenario, there will be a pot with the all-in of that player, and any other raise, more than the all-in,
* will be managed in a side pot.<br/>
* As part of it, we must be aware of the players in this class.
* @author Haim Adrian
* @since 12-Jun-21
*/
public class Pot {
/**
* Map between pot to a set of contributing players.<br/>
* This allows us to create side pots when there is a player that went all-int, and other players continue raising.
*/
@JsonSerialize(keyUsing = Player.PlayerKeySerializer.class)
@JsonDeserialize(keyUsing = Player.PlayerKeyDeserializer.class)
@JsonProperty
private final Map<Player, HandPot> pots = new HashMap<>();
/**
* Keep pots of players for a round of bets, so we can know the total bet of a player per round, and also
* get the ability to equal a bet when player that already paid is calling.
*/
@JsonSerialize(keyUsing = Player.PlayerKeySerializer.class)
@JsonDeserialize(keyUsing = Player.PlayerKeyDeserializer.class)
@JsonProperty
private final Map<Player, HandPot> potsForRound = new HashMap<>();
/**
* A reference to the last bet, to make sure a new bet is legal and not below it.<br/>
* It might be null when we just start a round, in this case the bet will be set according to the small bet player.
*/
@Getter
private Long lastBet;
/**
* This method is exposed so we can use it from {@link GameEngine#stop()}, to make sure we do not stop
* a game and lose chips. When there are pots and a game is stopped, the chips are returned to the players.
* @return A copy of all player pots
*/
@JsonIgnore
public Map<Player, Long> getPlayerPots() {
return pots.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getSum()));
}
/**
* Use this method bet / call.<br/>
* The method will return how many chips were actually been taken. It might be that a user used
* his all-in, which is less than the specified amount.
* @param player The player who is betting
* @param amount The bet amount
* @return The actual amount of chips that we used
* @throws IllegalArgumentException In case {@code amount} is smaller than last bet, and player has enough chips to equal he amount to last bet
*/
public long bet(Player player, long amount) {
// If amount is bigger than the ALL-IN of the specified player, use the player's ALL-IN.
long validatedAmount = Math.min(amount, player.getChips().get()) + getPotOfPlayer(player);
if (lastBet != null) {
if ((validatedAmount < lastBet) && (player.getChips().get() > validatedAmount)) {
throw new IllegalArgumentException("Cannot bet with amount smaller than last bet. [amount=" + validatedAmount + ", lastBet=" + lastBet + "]");
}
}
lastBet = amount + getPotOfPlayer(player);
long delta = validatedAmount - getPotOfPlayer(player);
player.getChips().remove(delta);
pots.computeIfAbsent(player, p -> new HandPot()).add(delta);
potsForRound.computeIfAbsent(player, p -> new HandPot()).add(delta);
return delta;
}
/**
* Get existing pot for player, to know how many chips a player have already paid.
* @param player The player to get its pot
* @return How many chips the specified player put. Can be 0.
*/
public long getPotOfPlayer(Player player) {
if (!potsForRound.containsKey(player)) {
return 0;
}
return potsForRound.get(player).sum;
}
/**
* Clear stored last bet to support resetting a bet round and start betting again.
*/
public void clearLastBet() {
lastBet = null;
}
/**
* Call this method when round of bets is over, so we will clear existing players pots.
*/
public void clearPotsOfRound() {
potsForRound.clear();
}
/**
* Clear all saved pots
*/
public void clear() {
pots.clear();
clearPotsOfRound();
clearLastBet();
}
/**
* Use this method when a round is over, and we need to share the pot among winning players.<br/>
* Note that not all of the money will get to the best hand, as it might be that the best hand belongs to a player
* that went all-in, and there is some dead money in the pot, of players that bet after the winner who went all-in.
* In this case, the "dead" money will be split between the other involved winners.
* @param involvedPlayers See {@link Players#getInvolvedPlayers()}
* @param board The {@link Board}, to find winning hands.
* @return A map between a winner and {@link PlayerWinning} reference holding the amount of chips and hand rank.
*/
public Map<String, PlayerWinning> applyWinning(Set<Player> involvedPlayers, Board board) {
Map<Player, PlayerWinning> result = new HashMap<>();
// Use a tree map so we will sort the map based on keys (ranks)
// We use a reverse order comparator, so the best rank will be first.
Map<HandRankCalculatorResult, Set<Player>> rankToPlayers = new TreeMap<>(Comparator.reverseOrder());
for (Player player : involvedPlayers) {
HandRankCalculatorResult rank = HandRankCalculator.calculate(board, player.getHand());
rankToPlayers.computeIfAbsent(rank, r -> new HashSet<>()).add(player);
}
for (Map.Entry<HandRankCalculatorResult, Set<Player>> currRankToPlayers : rankToPlayers.entrySet()) {
Set<Player> winners = currRankToPlayers.getValue();
// Keep spreading pots until we handle all winning players.
while (!winners.isEmpty()) {
// Get the minimum sum based on winners, to take this part out from pots, and share among winners.
long minSum = findMinSumBasedOnWinnersBet(winners);
Pair<Long, Long> winShareAndRemainder = calculateWinAndRemainder(winners.size(), minSum);
Player firstWinner = winners.iterator().next();
// Now iterate over all winners to share the pots among them.
sharePotsAmongWinners(result, winners, winShareAndRemainder.getFirst(), currRankToPlayers.getKey());
// Now add the remainder to the first player
if (firstWinner != null) {
firstWinner.getChips().add(winShareAndRemainder.getSecond());
result.get(firstWinner).sum += winShareAndRemainder.getSecond();
}
}
// If all pots have been drained, there is no reason to continue.
// We continue iterating when there is "dead money".
if (pots.isEmpty()) {
break;
}
}
// Clear the pots for next round
pots.clear();
return result.entrySet().stream().collect(Collectors.toMap(ent -> ent.getKey().getId(), Map.Entry::getValue));
}
/**
* A method to give for each winner the amount of chips he deserves.<br/>
* The amount of winning chips is calculated at {@link #calculateWinAndRemainder(int, long)}, and we
* just iterate over all winners and assign for each winner the amount of chips he deserves.<br/>
* The method will remove a winner from {@code winners}, in case it was fully handled (meaning its pot
* has drained and the player has already received all the chips he deserves)
* @param winnerToWinSum How many chips each player earned.
* @param winners Set of winners to go over and share the pots among them.
* @param winningSum To know how many chips each winner deserves
* @param handRank The hand rank of a winner, to save it to the value we put in {@code winnerToWinSum}
*/
private void sharePotsAmongWinners(Map<Player, PlayerWinning> winnerToWinSum, Set<Player> winners, long winningSum, HandRankCalculatorResult handRank) {
for (Iterator<Player> winnerIter = winners.iterator(); winnerIter.hasNext();) {
Player winner = winnerIter.next();
// If current player's pot has been drained, it means we've finished handling this player, hence
// we remove him.
if (pots.get(winner) == null) {
winnerIter.remove();
}
// Add the chips to the winner
winner.getChips().add(winningSum);
if (!winnerToWinSum.containsKey(winner)) {
winnerToWinSum.put(winner, new PlayerWinning(0L, handRank));
}
// Add the winning amount to the total amount of current winner.
// It might be that there will be several iterations, hence we aggregate.
winnerToWinSum.get(winner).sum += winningSum;
}
}
/**
* This method will go over the pots of the winners, to find the minimum sum, such
* that we can make a pot out of that sum to share among the winners.<br/>
* This is how we handle "dead money". (The "dead money" will stay in {@link #pots})
* @param winners Set of players to find minimum sum of
* @return The minimum sum
*/
private long findMinSumBasedOnWinnersBet(Set<Player> winners) {
long minSum = Long.MAX_VALUE;
for (Player currWinner : winners) {
long sum = pots.get(currWinner).getSum();
if (sum < minSum) {
minSum = sum;
}
}
return minSum;
}
/**
* This method calculates how many chips each winner earns, and the remainder, out of all pots.<br/>
* We take the amount of the specified {@code sumToTakeFromPots} out of all {@link #pots}, and then calculate
* the winning sum (totalSum / winners.size()), and the remainder.
* @param amountOfWinners Amount of winners to calculate how many chips each winner earns
* @param sumToTakeFromPots The amount of chips to take from each pot
* @return Pair(win, remainder)
*/
private Pair<Long, Long> calculateWinAndRemainder(int amountOfWinners, long sumToTakeFromPots) {
long allPots = 0;
for (Iterator<Map.Entry<Player, HandPot>> potsIterator = pots.entrySet().iterator(); potsIterator.hasNext();) {
Map.Entry<Player, HandPot> currPot = potsIterator.next();
allPots += currPot.getValue().takeSum(sumToTakeFromPots);
// If current pot has drained, remove it.
// We remove drained pots so we will know when to stop the main applyWinnings loop.
if (currPot.getValue().getSum() == 0) {
potsIterator.remove();
}
}
long win = allPots / amountOfWinners;
long remainder = allPots % amountOfWinners;
return Pair.of(win, remainder);
}
@JsonIgnore
public long sum() {
if (pots.isEmpty()) {
return 0;
}
return pots.values().stream().mapToLong(HandPot::getSum).sum();
}
/**
* Reference to a single pot.<br/>
* Usually there is one pot, but there might be more when players are out of chips.
*/
@Data
private static class HandPot {
private long sum = 0;
public void add(long amount) {
sum += amount;
}
/**
* Remove an amount out of sum and return it.<br/>
* This method is used when we take chips for a winning player, and we allow
* defining an upper bound to make sure we do not give a player more than he bet.
* @param upperBound The bound to set
* @return the sum, considering upper bound.
*/
public long takeSum(long upperBound) {
long result = Math.min(sum, upperBound);
if (upperBound >= sum) {
sum = 0;
} else {
sum -= upperBound;
}
return result;
}
}
/**
* A class to hold how many chips a player earns, and what his hand rank is.<br/>
* The hand rank is needed so we will be able to show what hand a winner had.
*/
@NoArgsConstructor
@AllArgsConstructor
public static class PlayerWinning {
/**
* How many chips a player earns
*/
@Getter
private long sum = 0;
/**
* The hand rank of a player, containing the selected cards, so client can show the hand rank of a winner
*/
@Getter
private HandRankCalculatorResult handRank;
}
}