@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.
Files changed (48) hide show
  1. package/README.md +722 -0
  2. package/as-test.config.js +36 -0
  3. package/asconfig.json +22 -0
  4. package/assembly/__tests__/blackjack/actions/common.spec.ts +180 -0
  5. package/assembly/__tests__/blackjack/actions/dealer_scenarios.spec.ts +452 -0
  6. package/assembly/__tests__/blackjack/actions/double.spec.ts +128 -0
  7. package/assembly/__tests__/blackjack/actions/edge_cases.spec.ts +1041 -0
  8. package/assembly/__tests__/blackjack/actions/insurance.spec.ts +39 -0
  9. package/assembly/__tests__/blackjack/actions/split.spec.ts +96 -0
  10. package/assembly/__tests__/blackjack/actions/stand.spec.ts +103 -0
  11. package/assembly/__tests__/blackjack/actions/surrender.spec.ts +89 -0
  12. package/assembly/__tests__/blackjack/actions/test.ts +18 -0
  13. package/assembly/__tests__/blackjack/rules.spec.ts +231 -0
  14. package/assembly/__tests__/deck/deck.spec.ts +551 -0
  15. package/assembly/__tests__/deck/shoe.spec.ts +410 -0
  16. package/assembly/__tests__/poker/betting_round.spec.ts +103 -0
  17. package/assembly/__tests__/poker/omaha.spec.ts +171 -0
  18. package/assembly/__tests__/poker/pots.spec.ts +255 -0
  19. package/assembly/__tests__/poker/showdown.spec.ts +324 -0
  20. package/assembly/__tests__/poker/six_plus.spec.ts +152 -0
  21. package/assembly/__tests__/poker/stakes.spec.ts +384 -0
  22. package/assembly/__tests__/poker/stud.spec.ts +190 -0
  23. package/assembly/__tests__/poker/test.ts +13 -0
  24. package/assembly/__tests__/test.ts +11 -0
  25. package/assembly/blackjack/actions.ts +191 -0
  26. package/assembly/blackjack/blackjack.ts +571 -0
  27. package/assembly/blackjack/rules.ts +11 -0
  28. package/assembly/cardgames.ts +314 -0
  29. package/assembly/cards.ts +314 -0
  30. package/assembly/cashgames/cash_game_types.ts +142 -0
  31. package/assembly/cashgames/cash_game_utils.ts +223 -0
  32. package/assembly/cashgames/index.ts +10 -0
  33. package/assembly/deck/deck.ts +744 -0
  34. package/assembly/deck/index.ts +9 -0
  35. package/assembly/index.ts +28 -0
  36. package/assembly/poker/index.ts +17 -0
  37. package/assembly/poker/omaha_evaluator.ts +121 -0
  38. package/assembly/poker/poker_game_types.ts +233 -0
  39. package/assembly/poker/poker_game_utils.ts +671 -0
  40. package/assembly/poker/showdown.ts +106 -0
  41. package/assembly/poker/showdown_evaluator.ts +225 -0
  42. package/assembly/poker/six_plus_showdown.ts +96 -0
  43. package/assembly/poker/stud_evaluator.ts +60 -0
  44. package/assembly/poker/variant_utils.ts +51 -0
  45. package/assembly/poker/variants.ts +182 -0
  46. package/assembly/poker.ts +307 -0
  47. package/package.json +51 -0
  48. 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
+