@arcanahq/cardgames 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +722 -0
- package/as-test.config.js +36 -0
- package/asconfig.json +22 -0
- package/assembly/__tests__/blackjack/actions/common.spec.ts +180 -0
- package/assembly/__tests__/blackjack/actions/dealer_scenarios.spec.ts +452 -0
- package/assembly/__tests__/blackjack/actions/double.spec.ts +128 -0
- package/assembly/__tests__/blackjack/actions/edge_cases.spec.ts +1041 -0
- package/assembly/__tests__/blackjack/actions/insurance.spec.ts +39 -0
- package/assembly/__tests__/blackjack/actions/split.spec.ts +96 -0
- package/assembly/__tests__/blackjack/actions/stand.spec.ts +103 -0
- package/assembly/__tests__/blackjack/actions/surrender.spec.ts +89 -0
- package/assembly/__tests__/blackjack/actions/test.ts +18 -0
- package/assembly/__tests__/blackjack/rules.spec.ts +231 -0
- package/assembly/__tests__/deck/deck.spec.ts +551 -0
- package/assembly/__tests__/deck/shoe.spec.ts +410 -0
- package/assembly/__tests__/poker/betting_round.spec.ts +103 -0
- package/assembly/__tests__/poker/omaha.spec.ts +171 -0
- package/assembly/__tests__/poker/pots.spec.ts +255 -0
- package/assembly/__tests__/poker/showdown.spec.ts +324 -0
- package/assembly/__tests__/poker/six_plus.spec.ts +152 -0
- package/assembly/__tests__/poker/stakes.spec.ts +384 -0
- package/assembly/__tests__/poker/stud.spec.ts +190 -0
- package/assembly/__tests__/poker/test.ts +13 -0
- package/assembly/__tests__/test.ts +11 -0
- package/assembly/blackjack/actions.ts +191 -0
- package/assembly/blackjack/blackjack.ts +571 -0
- package/assembly/blackjack/rules.ts +11 -0
- package/assembly/cardgames.ts +314 -0
- package/assembly/cards.ts +314 -0
- package/assembly/cashgames/cash_game_types.ts +142 -0
- package/assembly/cashgames/cash_game_utils.ts +223 -0
- package/assembly/cashgames/index.ts +10 -0
- package/assembly/deck/deck.ts +744 -0
- package/assembly/deck/index.ts +9 -0
- package/assembly/index.ts +28 -0
- package/assembly/poker/index.ts +17 -0
- package/assembly/poker/omaha_evaluator.ts +121 -0
- package/assembly/poker/poker_game_types.ts +233 -0
- package/assembly/poker/poker_game_utils.ts +671 -0
- package/assembly/poker/showdown.ts +106 -0
- package/assembly/poker/showdown_evaluator.ts +225 -0
- package/assembly/poker/six_plus_showdown.ts +96 -0
- package/assembly/poker/stud_evaluator.ts +60 -0
- package/assembly/poker/variant_utils.ts +51 -0
- package/assembly/poker/variants.ts +182 -0
- package/assembly/poker.ts +307 -0
- package/package.json +51 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Poker Game Utilities
|
|
4
|
+
*
|
|
5
|
+
* Generic utilities for poker-style card games including:
|
|
6
|
+
* - Blinds and ante posting
|
|
7
|
+
* - Pot construction and side pots
|
|
8
|
+
* - Rake calculation
|
|
9
|
+
* - Pot distribution
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Pot, Stakes, AnteType, PokerRakeConfig, PotDistributionResult, BettingRoundState, PokerSeatBase } from "./poker_game_types";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Calculate rake for a pot amount
|
|
16
|
+
* @param potAmount Total pot amount
|
|
17
|
+
* @param rakeConfig Rake configuration
|
|
18
|
+
* @returns Rake amount to deduct
|
|
19
|
+
*/
|
|
20
|
+
export function calculatePokerRake(potAmount: i64, rakeConfig: PokerRakeConfig): i64 {
|
|
21
|
+
if (rakeConfig.percentage <= 0.0) {
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Calculate rake as percentage
|
|
26
|
+
const rake = <i64>(<f64>potAmount * rakeConfig.percentage / 100.0);
|
|
27
|
+
|
|
28
|
+
// Apply cap if set
|
|
29
|
+
if (rakeConfig.cap > 0 && rake > rakeConfig.cap) {
|
|
30
|
+
return rakeConfig.cap;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return rake;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Calculate rake for a pot amount (simple version with percentage and cap)
|
|
38
|
+
* @param potAmount Total pot amount
|
|
39
|
+
* @param rakePercentage Rake percentage (e.g., 5.0 for 5%)
|
|
40
|
+
* @param rakeCap Maximum rake per pot (0 = no cap)
|
|
41
|
+
* @returns Rake amount to deduct
|
|
42
|
+
*/
|
|
43
|
+
export function calculateRakeSimple(potAmount: i64, rakePercentage: f64, rakeCap: i64): i64 {
|
|
44
|
+
if (rakePercentage <= 0.0) {
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Calculate rake as percentage
|
|
49
|
+
const rake = <i64>(<f64>potAmount * rakePercentage / 100.0);
|
|
50
|
+
|
|
51
|
+
// Apply cap if set
|
|
52
|
+
if (rakeCap > 0 && rake > rakeCap) {
|
|
53
|
+
return rakeCap;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return rake;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Construct side pots from player contributions
|
|
61
|
+
* Creates separate pots for each contribution level (for all-in scenarios)
|
|
62
|
+
*
|
|
63
|
+
* @param contributions Map of seat ID to total contribution
|
|
64
|
+
* @param startPotId Starting pot ID (usually 0)
|
|
65
|
+
* @returns Array of Pot objects, ordered from smallest to largest
|
|
66
|
+
*/
|
|
67
|
+
export function constructSidePots(contributions: Map<i32, i64>, startPotId: i32 = 0): Pot[] {
|
|
68
|
+
const pots = new Array<Pot>(0);
|
|
69
|
+
|
|
70
|
+
if (contributions.size === 0) {
|
|
71
|
+
return pots;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Get unique contribution levels, sorted ascending
|
|
75
|
+
const contribValues = new Array<i64>(0);
|
|
76
|
+
const contribKeys = contributions.keys();
|
|
77
|
+
for (let i = 0; i < contribKeys.length; i++) {
|
|
78
|
+
const value = contributions.get(contribKeys[i]);
|
|
79
|
+
let found = false;
|
|
80
|
+
for (let j = 0; j < contribValues.length; j++) {
|
|
81
|
+
if (contribValues[j] === value) {
|
|
82
|
+
found = true;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!found) {
|
|
87
|
+
contribValues.push(value);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Sort ascending
|
|
92
|
+
contribValues.sort((a, b) => a < b ? -1 : (a > b ? 1 : 0));
|
|
93
|
+
|
|
94
|
+
// Create pots for each contribution level
|
|
95
|
+
let prevLevel: i64 = 0;
|
|
96
|
+
for (let i = 0; i < contribValues.length; i++) {
|
|
97
|
+
const level = contribValues[i];
|
|
98
|
+
const eligibleSeats = new Array<i32>(0);
|
|
99
|
+
|
|
100
|
+
// Find all seats with contribution >= this level
|
|
101
|
+
const seatIds = contributions.keys();
|
|
102
|
+
for (let j = 0; j < seatIds.length; j++) {
|
|
103
|
+
const seatId = seatIds[j];
|
|
104
|
+
const contrib = contributions.get(seatId);
|
|
105
|
+
if (contrib >= level) {
|
|
106
|
+
eligibleSeats.push(seatId);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Calculate pot amount for this level
|
|
111
|
+
// Each eligible seat contributes (level - prevLevel) to this pot
|
|
112
|
+
const amountPerSeat = level - prevLevel;
|
|
113
|
+
let potAmount: i64 = 0;
|
|
114
|
+
|
|
115
|
+
// Count how many seats contribute to this pot
|
|
116
|
+
for (let j = 0; j < seatIds.length; j++) {
|
|
117
|
+
const seatId = seatIds[j];
|
|
118
|
+
const contrib = contributions.get(seatId);
|
|
119
|
+
if (contrib >= prevLevel) {
|
|
120
|
+
const contribToThisPot = contrib >= level ? amountPerSeat : (contrib - prevLevel);
|
|
121
|
+
potAmount += contribToThisPot;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (potAmount > 0 && eligibleSeats.length > 0) {
|
|
126
|
+
const pot = new Pot(startPotId + pots.length, potAmount, eligibleSeats, false, 1, new Array<i64>(0));
|
|
127
|
+
pots.push(pot);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
prevLevel = level;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return pots;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Split a pot for run-it multiple times
|
|
138
|
+
* @param pot Pot to split
|
|
139
|
+
* @param runCount Number of runs
|
|
140
|
+
* @returns New pot with split amounts
|
|
141
|
+
*/
|
|
142
|
+
export function splitPotForRuns(pot: Pot, runCount: i32): Pot {
|
|
143
|
+
if (runCount <= 1) {
|
|
144
|
+
const newPot = pot.clone();
|
|
145
|
+
newPot.splitAmountsPerRun = new Array<i64>(1);
|
|
146
|
+
newPot.splitAmountsPerRun[0] = pot.amount;
|
|
147
|
+
newPot.runCountForPot = 1;
|
|
148
|
+
return newPot;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const base = pot.amount / <i64>runCount;
|
|
152
|
+
const remainder = pot.amount % <i64>runCount;
|
|
153
|
+
|
|
154
|
+
const splits = new Array<i64>(runCount);
|
|
155
|
+
for (let i = 0; i < runCount; i++) {
|
|
156
|
+
splits[i] = base + (i < remainder ? 1 : 0);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const newPot = pot.clone();
|
|
160
|
+
newPot.runCountForPot = runCount;
|
|
161
|
+
newPot.splitAmountsPerRun = splits;
|
|
162
|
+
|
|
163
|
+
return newPot;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Split all pots for run-it multiple times
|
|
168
|
+
* @param pots Array of pots to split
|
|
169
|
+
* @param runCount Number of runs
|
|
170
|
+
* @returns Array of split pots
|
|
171
|
+
*/
|
|
172
|
+
export function splitAllPotsForRuns(pots: Pot[], runCount: i32): Pot[] {
|
|
173
|
+
const newPots = new Array<Pot>(pots.length);
|
|
174
|
+
for (let i = 0; i < pots.length; i++) {
|
|
175
|
+
newPots[i] = splitPotForRuns(pots[i], runCount);
|
|
176
|
+
}
|
|
177
|
+
return newPots;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Calculate odd chip distribution
|
|
182
|
+
* Deterministic: distributes remainder chips to winners
|
|
183
|
+
*
|
|
184
|
+
* @param amount Total amount to distribute
|
|
185
|
+
* @param numWinners Number of winners
|
|
186
|
+
* @param winners Array of winner seat IDs
|
|
187
|
+
* @param buttonSeatId Button seat ID (for odd chip distribution rules)
|
|
188
|
+
* @returns Array of payout amounts, one per winner
|
|
189
|
+
*/
|
|
190
|
+
export function calculateOddChips(amount: i64, numWinners: i32, winners: i32[], buttonSeatId: i32): i64[] {
|
|
191
|
+
if (numWinners <= 0) {
|
|
192
|
+
return new Array<i64>(0);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const base = amount / <i64>numWinners;
|
|
196
|
+
const remainder = amount % <i64>numWinners;
|
|
197
|
+
|
|
198
|
+
const payouts = new Array<i64>(numWinners);
|
|
199
|
+
for (let i = 0; i < numWinners; i++) {
|
|
200
|
+
payouts[i] = base;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Distribute remainder chips to winners closest to button (clockwise)
|
|
204
|
+
// For simplicity, give remainder to first winner in array
|
|
205
|
+
// In full implementation, would calculate actual clockwise distance from button
|
|
206
|
+
if (remainder > 0) {
|
|
207
|
+
payouts[0] += remainder;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return payouts;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Distribute pot to winners (after rake deduction)
|
|
215
|
+
*
|
|
216
|
+
* @param pot Pot to distribute
|
|
217
|
+
* @param winners Array of winner seat IDs
|
|
218
|
+
* @param runIndex Run index (for run-it multiple times, 0-based)
|
|
219
|
+
* @param buttonSeatId Button seat ID (for odd chip distribution)
|
|
220
|
+
* @param rakeConfig Rake configuration
|
|
221
|
+
* @returns PotDistributionResult with payouts and rake
|
|
222
|
+
*/
|
|
223
|
+
export function distributePot(
|
|
224
|
+
pot: Pot,
|
|
225
|
+
winners: i32[],
|
|
226
|
+
runIndex: i32,
|
|
227
|
+
buttonSeatId: i32,
|
|
228
|
+
rakeConfig: PokerRakeConfig
|
|
229
|
+
): PotDistributionResult {
|
|
230
|
+
const payouts = new Map<i32, i64>();
|
|
231
|
+
|
|
232
|
+
if (winners.length === 0 || pot.splitAmountsPerRun.length === 0) {
|
|
233
|
+
return new PotDistributionResult(payouts, 0);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const runAmount = runIndex < pot.splitAmountsPerRun.length
|
|
237
|
+
? pot.splitAmountsPerRun[runIndex]
|
|
238
|
+
: pot.amount / <i64>pot.runCountForPot;
|
|
239
|
+
|
|
240
|
+
// Calculate and deduct rake
|
|
241
|
+
const rake = calculatePokerRake(runAmount, rakeConfig);
|
|
242
|
+
const potAfterRake = runAmount - rake;
|
|
243
|
+
|
|
244
|
+
// Distribute remaining pot to winners
|
|
245
|
+
const oddChips = calculateOddChips(potAfterRake, winners.length, winners, buttonSeatId);
|
|
246
|
+
|
|
247
|
+
for (let i = 0; i < winners.length; i++) {
|
|
248
|
+
payouts.set(winners[i], oddChips[i]);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return new PotDistributionResult(payouts, rake);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Distribute pot to winners (simple version with percentage and cap)
|
|
256
|
+
*
|
|
257
|
+
* @param pot Pot to distribute
|
|
258
|
+
* @param winners Array of winner seat IDs
|
|
259
|
+
* @param runIndex Run index (for run-it multiple times, 0-based)
|
|
260
|
+
* @param buttonSeatId Button seat ID (for odd chip distribution)
|
|
261
|
+
* @param rakePercentage Rake percentage (e.g., 5.0 for 5%)
|
|
262
|
+
* @param rakeCap Maximum rake per pot (0 = no cap)
|
|
263
|
+
* @returns PotDistributionResult with payouts and rake
|
|
264
|
+
*/
|
|
265
|
+
export function distributePotSimple(
|
|
266
|
+
pot: Pot,
|
|
267
|
+
winners: i32[],
|
|
268
|
+
runIndex: i32,
|
|
269
|
+
buttonSeatId: i32,
|
|
270
|
+
rakePercentage: f64 = 0.0,
|
|
271
|
+
rakeCap: i64 = 0
|
|
272
|
+
): PotDistributionResult {
|
|
273
|
+
const rakeConfig = new PokerRakeConfig(rakePercentage, rakeCap, false);
|
|
274
|
+
return distributePot(pot, winners, runIndex, buttonSeatId, rakeConfig);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Lock pots (no further contributions can affect eligibility)
|
|
279
|
+
* @param pots Array of pots to lock
|
|
280
|
+
* @returns Array of locked pots
|
|
281
|
+
*/
|
|
282
|
+
export function lockPots(pots: Pot[]): Pot[] {
|
|
283
|
+
const lockedPots = new Array<Pot>(pots.length);
|
|
284
|
+
for (let i = 0; i < pots.length; i++) {
|
|
285
|
+
const pot = pots[i].clone();
|
|
286
|
+
pot.locked = true;
|
|
287
|
+
lockedPots[i] = pot;
|
|
288
|
+
}
|
|
289
|
+
return lockedPots;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Calculate total pot amount from all pots
|
|
294
|
+
* @param pots Array of pots
|
|
295
|
+
* @returns Total amount across all pots
|
|
296
|
+
*/
|
|
297
|
+
export function getTotalPotAmount(pots: Pot[]): i64 {
|
|
298
|
+
let total: i64 = 0;
|
|
299
|
+
for (let i = 0; i < pots.length; i++) {
|
|
300
|
+
total += pots[i].amount;
|
|
301
|
+
}
|
|
302
|
+
return total;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Calculate ante amount based on ante type
|
|
307
|
+
* @param stakes Stakes configuration
|
|
308
|
+
* @param anteType Ante type (NONE, FIXED, PERCENTAGE)
|
|
309
|
+
* @param antePercentage If PERCENTAGE type, the percentage value (e.g., 10.0 for 10%)
|
|
310
|
+
* @returns Ante amount per player
|
|
311
|
+
*/
|
|
312
|
+
export function calculateAnteAmount(stakes: Stakes, anteType: string, antePercentage: f64 = 0.0): i64 {
|
|
313
|
+
if (anteType === AnteType.NONE) {
|
|
314
|
+
return 0;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (anteType === AnteType.FIXED) {
|
|
318
|
+
return stakes.ante;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (anteType === AnteType.PERCENTAGE) {
|
|
322
|
+
// Ante as percentage of big blind
|
|
323
|
+
return <i64>(<f64>stakes.bb * antePercentage / 100.0);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return 0;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Post antes for all active players in hand
|
|
331
|
+
* Subtracts ante from each player's stack and adds to contributions
|
|
332
|
+
*
|
|
333
|
+
* @param seats Array of poker seats (must have stack property)
|
|
334
|
+
* @param stakes Stakes configuration
|
|
335
|
+
* @param anteType Ante type (NONE, FIXED, PERCENTAGE)
|
|
336
|
+
* @param antePercentage If PERCENTAGE type, the percentage value
|
|
337
|
+
* @param bettingState Betting round state to update contributions
|
|
338
|
+
* @returns Updated seats array and total ante collected
|
|
339
|
+
*/
|
|
340
|
+
export class PostAntesResult {
|
|
341
|
+
seats: PokerSeatBase[];
|
|
342
|
+
totalAnteCollected: i64;
|
|
343
|
+
|
|
344
|
+
constructor(seats: PokerSeatBase[], totalAnteCollected: i64) {
|
|
345
|
+
this.seats = seats;
|
|
346
|
+
this.totalAnteCollected = totalAnteCollected;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function postAntes(
|
|
351
|
+
seats: PokerSeatBase[],
|
|
352
|
+
stakes: Stakes,
|
|
353
|
+
anteType: string,
|
|
354
|
+
antePercentage: f64,
|
|
355
|
+
bettingState: BettingRoundState
|
|
356
|
+
): PostAntesResult {
|
|
357
|
+
const anteAmount = calculateAnteAmount(stakes, anteType, antePercentage);
|
|
358
|
+
let totalAnteCollected: i64 = 0;
|
|
359
|
+
|
|
360
|
+
const updatedSeats = new Array<PokerSeatBase>(seats.length);
|
|
361
|
+
|
|
362
|
+
for (let i = 0; i < seats.length; i++) {
|
|
363
|
+
const seat = seats[i].clone();
|
|
364
|
+
|
|
365
|
+
// Only post ante for players in hand
|
|
366
|
+
if (seat.inHand && !seat.isEmpty()) {
|
|
367
|
+
const anteToPost = anteAmount;
|
|
368
|
+
|
|
369
|
+
// Subtract from stack (can't go negative)
|
|
370
|
+
if (seat.stack >= anteToPost) {
|
|
371
|
+
seat.stack -= anteToPost;
|
|
372
|
+
totalAnteCollected += anteToPost;
|
|
373
|
+
|
|
374
|
+
// Add to contributions
|
|
375
|
+
const currentContrib = bettingState.getContributionThisRound(seat.seatId);
|
|
376
|
+
bettingState.contribThisRound.set(seat.seatId, currentContrib + anteToPost);
|
|
377
|
+
|
|
378
|
+
const totalContrib = bettingState.getTotalContribution(seat.seatId);
|
|
379
|
+
bettingState.contribTotal.set(seat.seatId, totalContrib + anteToPost);
|
|
380
|
+
} else {
|
|
381
|
+
// All-in ante
|
|
382
|
+
const remainingStack = seat.stack;
|
|
383
|
+
seat.stack = 0;
|
|
384
|
+
seat.allIn = true;
|
|
385
|
+
totalAnteCollected += remainingStack;
|
|
386
|
+
|
|
387
|
+
// Add to contributions
|
|
388
|
+
const currentContrib = bettingState.getContributionThisRound(seat.seatId);
|
|
389
|
+
bettingState.contribThisRound.set(seat.seatId, currentContrib + remainingStack);
|
|
390
|
+
|
|
391
|
+
const totalContrib = bettingState.getTotalContribution(seat.seatId);
|
|
392
|
+
bettingState.contribTotal.set(seat.seatId, totalContrib + remainingStack);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
updatedSeats[i] = seat;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return new PostAntesResult(updatedSeats, totalAnteCollected);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Post small blind and big blind
|
|
404
|
+
* Subtracts blinds from players' stacks and adds to contributions
|
|
405
|
+
*
|
|
406
|
+
* @param seats Array of poker seats
|
|
407
|
+
* @param stakes Stakes configuration
|
|
408
|
+
* @param sbSeatId Small blind seat ID
|
|
409
|
+
* @param bbSeatId Big blind seat ID
|
|
410
|
+
* @param bettingState Betting round state to update contributions
|
|
411
|
+
* @returns Updated seats array and updated currentBetToMatch
|
|
412
|
+
*/
|
|
413
|
+
export class PostBlindsResult {
|
|
414
|
+
seats: PokerSeatBase[];
|
|
415
|
+
currentBetToMatch: i64;
|
|
416
|
+
|
|
417
|
+
constructor(seats: PokerSeatBase[], currentBetToMatch: i64) {
|
|
418
|
+
this.seats = seats;
|
|
419
|
+
this.currentBetToMatch = currentBetToMatch;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function postBlinds(
|
|
424
|
+
seats: PokerSeatBase[],
|
|
425
|
+
stakes: Stakes,
|
|
426
|
+
sbSeatId: i32,
|
|
427
|
+
bbSeatId: i32,
|
|
428
|
+
bettingState: BettingRoundState
|
|
429
|
+
): PostBlindsResult {
|
|
430
|
+
const updatedSeats = new Array<PokerSeatBase>(seats.length);
|
|
431
|
+
let currentBetToMatch: i64 = 0;
|
|
432
|
+
|
|
433
|
+
// Copy seats first
|
|
434
|
+
for (let i = 0; i < seats.length; i++) {
|
|
435
|
+
updatedSeats[i] = seats[i].clone();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Post small blind
|
|
439
|
+
if (sbSeatId >= 0 && sbSeatId < updatedSeats.length) {
|
|
440
|
+
const sbSeat = updatedSeats[sbSeatId];
|
|
441
|
+
if (sbSeat.inHand && !sbSeat.isEmpty()) {
|
|
442
|
+
const sbAmount = stakes.sb;
|
|
443
|
+
|
|
444
|
+
if (sbSeat.stack >= sbAmount) {
|
|
445
|
+
sbSeat.stack -= sbAmount;
|
|
446
|
+
} else {
|
|
447
|
+
// All-in small blind
|
|
448
|
+
sbSeat.stack = 0;
|
|
449
|
+
sbSeat.allIn = true;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const actualSbAmount = sbSeat.stack === 0 ? (sbAmount - (sbSeat.stack + sbAmount - sbAmount)) : sbAmount;
|
|
453
|
+
const actualSb = actualSbAmount > sbSeat.stack + sbAmount ? sbSeat.stack + sbAmount : actualSbAmount;
|
|
454
|
+
const postedSb = sbSeat.stack === 0 ? (sbSeat.stack + sbAmount) : sbAmount;
|
|
455
|
+
|
|
456
|
+
// Calculate actual amount posted
|
|
457
|
+
const sbPosted = sbSeat.stack === 0 ? (sbAmount - (sbSeat.stack + sbAmount - sbAmount)) : sbAmount;
|
|
458
|
+
const finalSb = sbSeat.stack === 0 ? (sbSeat.stack + sbAmount) : sbAmount;
|
|
459
|
+
|
|
460
|
+
// Simplified: just use the amount we subtracted
|
|
461
|
+
const sbPostedAmount = sbSeat.allIn ? (sbAmount - (sbSeat.stack + sbAmount)) : sbAmount;
|
|
462
|
+
const finalSbPosted = sbSeat.allIn ? (sbSeat.stack + sbAmount) : sbAmount;
|
|
463
|
+
|
|
464
|
+
// Actually, let's recalculate properly
|
|
465
|
+
const originalStack = seats[sbSeatId].stack;
|
|
466
|
+
const sbPostedFinal = originalStack >= sbAmount ? sbAmount : originalStack;
|
|
467
|
+
|
|
468
|
+
// Add to contributions
|
|
469
|
+
const currentContrib = bettingState.getContributionThisRound(sbSeatId);
|
|
470
|
+
bettingState.contribThisRound.set(sbSeatId, currentContrib + sbPostedFinal);
|
|
471
|
+
|
|
472
|
+
const totalContrib = bettingState.getTotalContribution(sbSeatId);
|
|
473
|
+
bettingState.contribTotal.set(sbSeatId, totalContrib + sbPostedFinal);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Post big blind
|
|
478
|
+
if (bbSeatId >= 0 && bbSeatId < updatedSeats.length) {
|
|
479
|
+
const bbSeat = updatedSeats[bbSeatId];
|
|
480
|
+
if (bbSeat.inHand && !bbSeat.isEmpty()) {
|
|
481
|
+
const bbAmount = stakes.bb;
|
|
482
|
+
const originalStack = seats[bbSeatId].stack;
|
|
483
|
+
|
|
484
|
+
if (bbSeat.stack >= bbAmount) {
|
|
485
|
+
bbSeat.stack -= bbAmount;
|
|
486
|
+
} else {
|
|
487
|
+
// All-in big blind
|
|
488
|
+
bbSeat.stack = 0;
|
|
489
|
+
bbSeat.allIn = true;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const bbPostedFinal = originalStack >= bbAmount ? bbAmount : originalStack;
|
|
493
|
+
|
|
494
|
+
// Add to contributions
|
|
495
|
+
const currentContrib = bettingState.getContributionThisRound(bbSeatId);
|
|
496
|
+
bettingState.contribThisRound.set(bbSeatId, currentContrib + bbPostedFinal);
|
|
497
|
+
|
|
498
|
+
const totalContrib = bettingState.getTotalContribution(bbSeatId);
|
|
499
|
+
bettingState.contribTotal.set(bbSeatId, totalContrib + bbPostedFinal);
|
|
500
|
+
|
|
501
|
+
// Big blind sets the current bet to match
|
|
502
|
+
currentBetToMatch = bbPostedFinal;
|
|
503
|
+
bettingState.currentBetToMatch = currentBetToMatch;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return new PostBlindsResult(updatedSeats, currentBetToMatch);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Get next acting seat (clockwise from button, skipping folded/all-in players)
|
|
512
|
+
*
|
|
513
|
+
* @param seats Array of poker seats
|
|
514
|
+
* @param buttonSeatId Button seat ID
|
|
515
|
+
* @param isPreflop Whether this is preflop (action starts after big blind)
|
|
516
|
+
* @param bettingState Betting round state
|
|
517
|
+
* @returns Next acting seat ID, or -1 if no one can act
|
|
518
|
+
*/
|
|
519
|
+
export function getNextActingSeat(
|
|
520
|
+
seats: PokerSeatBase[],
|
|
521
|
+
buttonSeatId: i32,
|
|
522
|
+
isPreflop: bool,
|
|
523
|
+
bettingState: BettingRoundState
|
|
524
|
+
): i32 {
|
|
525
|
+
if (seats.length === 0) {
|
|
526
|
+
return -1;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Find starting position (after button for preflop, button for postflop)
|
|
530
|
+
let startIndex: i32 = -1;
|
|
531
|
+
if (isPreflop) {
|
|
532
|
+
// Preflop: start after big blind (button + 3)
|
|
533
|
+
startIndex = (buttonSeatId + 3) % seats.length;
|
|
534
|
+
} else {
|
|
535
|
+
// Postflop: start after button (button + 1)
|
|
536
|
+
startIndex = (buttonSeatId + 1) % seats.length;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Search clockwise for next player who can act
|
|
540
|
+
for (let i = 0; i < seats.length; i++) {
|
|
541
|
+
const seatIndex = (startIndex + i) % seats.length;
|
|
542
|
+
const seat = seats[seatIndex];
|
|
543
|
+
|
|
544
|
+
// Skip empty seats, folded players, and all-in players
|
|
545
|
+
if (seat.isEmpty() || !seat.inHand || seat.allIn) {
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Check if they need to act (haven't matched the bet)
|
|
550
|
+
const contrib = bettingState.getContributionThisRound(seat.seatId);
|
|
551
|
+
if (contrib < bettingState.currentBetToMatch || !seat.hasActedThisRound) {
|
|
552
|
+
return seat.seatId;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// No one needs to act
|
|
557
|
+
return -1;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Validate buy-in amount
|
|
562
|
+
*
|
|
563
|
+
* @param buyInAmount Amount player wants to buy in for
|
|
564
|
+
* @param minBuyIn Minimum buy-in amount
|
|
565
|
+
* @param maxBuyIn Maximum buy-in amount
|
|
566
|
+
* @returns true if buy-in amount is valid
|
|
567
|
+
*/
|
|
568
|
+
export function validateBuyIn(buyInAmount: i64, minBuyIn: i64, maxBuyIn: i64): bool {
|
|
569
|
+
return buyInAmount >= minBuyIn && buyInAmount <= maxBuyIn;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Process a buy-in for a player
|
|
574
|
+
* Adds chips to their stack and records buy-in amount
|
|
575
|
+
*
|
|
576
|
+
* @param seat Poker seat to update
|
|
577
|
+
* @param buyInAmount Amount to buy in for
|
|
578
|
+
* @param minBuyIn Minimum buy-in amount
|
|
579
|
+
* @param maxBuyIn Maximum buy-in amount
|
|
580
|
+
* @returns Updated seat, or null if buy-in is invalid
|
|
581
|
+
*/
|
|
582
|
+
export function processBuyIn(
|
|
583
|
+
seat: PokerSeatBase,
|
|
584
|
+
buyInAmount: i64,
|
|
585
|
+
minBuyIn: i64,
|
|
586
|
+
maxBuyIn: i64
|
|
587
|
+
): PokerSeatBase | null {
|
|
588
|
+
if (!validateBuyIn(buyInAmount, minBuyIn, maxBuyIn)) {
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const updatedSeat = seat.clone();
|
|
593
|
+
updatedSeat.stack += buyInAmount;
|
|
594
|
+
// Note: buyInAmount tracking would be in game-specific seat class
|
|
595
|
+
return updatedSeat;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Check if betting round is complete
|
|
600
|
+
* All non-all-in players must have acted and matched the bet
|
|
601
|
+
*
|
|
602
|
+
* @param seats Array of poker seats
|
|
603
|
+
* @param bettingState Betting round state
|
|
604
|
+
* @returns true if betting round is complete
|
|
605
|
+
*/
|
|
606
|
+
export function isBettingRoundComplete(
|
|
607
|
+
seats: PokerSeatBase[],
|
|
608
|
+
bettingState: BettingRoundState
|
|
609
|
+
): bool {
|
|
610
|
+
// Count active players (in hand, not all-in)
|
|
611
|
+
let activePlayers = 0;
|
|
612
|
+
let playersWhoCanAct = 0;
|
|
613
|
+
|
|
614
|
+
for (let i = 0; i < seats.length; i++) {
|
|
615
|
+
const seat = seats[i];
|
|
616
|
+
if (seat.isEmpty() || !seat.inHand) {
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
activePlayers++;
|
|
621
|
+
|
|
622
|
+
// Skip all-in players
|
|
623
|
+
if (seat.allIn) {
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
playersWhoCanAct++;
|
|
628
|
+
|
|
629
|
+
// Check if they've matched the bet
|
|
630
|
+
const contrib = bettingState.getContributionThisRound(seat.seatId);
|
|
631
|
+
if (contrib < bettingState.currentBetToMatch) {
|
|
632
|
+
return false; // Someone hasn't matched
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Check if they've acted (unless they're the last aggressor)
|
|
636
|
+
if (!seat.hasActedThisRound) {
|
|
637
|
+
// If this is the last aggressor and everyone else has matched, round is complete
|
|
638
|
+
if (seat.seatId === bettingState.lastAggressorSeatId) {
|
|
639
|
+
// Check if everyone else has matched
|
|
640
|
+
let allMatched = true;
|
|
641
|
+
for (let j = 0; j < seats.length; j++) {
|
|
642
|
+
if (i === j) continue;
|
|
643
|
+
const otherSeat = seats[j];
|
|
644
|
+
if (otherSeat.isEmpty() || !otherSeat.inHand || otherSeat.allIn) continue;
|
|
645
|
+
const otherContrib = bettingState.getContributionThisRound(otherSeat.seatId);
|
|
646
|
+
if (otherContrib < bettingState.currentBetToMatch) {
|
|
647
|
+
allMatched = false;
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (allMatched) {
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return false; // Someone hasn't acted
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// If only one active player, round is complete
|
|
660
|
+
if (activePlayers <= 1) {
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// If no one can act, round is complete
|
|
665
|
+
if (playersWhoCanAct === 0) {
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return true;
|
|
670
|
+
}
|
|
671
|
+
|