@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,744 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Generic Deck Management Library
4
+ *
5
+ * Provides deterministic shuffling and dealing for card games.
6
+ * Supports both standard 52-card decks and Spanish 21 (48-card) decks.
7
+ * Uses index-based card mapping for efficient storage and dealing.
8
+ *
9
+ * Efficient batch dealing (WASM instruction limit):
10
+ * - getShuffledIndices / getShuffledShoeIndices: one shuffle, return indices; index into result for multiple cards.
11
+ * - dealCards / dealCardsFromShoe: deal N cards with at most 1–2 shuffles (2 only when crossing deck/shoe boundary).
12
+ * Use these instead of calling dealCardByIndex / dealCardFromShoe in a loop.
13
+ */
14
+
15
+ import { Card, Suit, Rank } from "../cards";
16
+ import { getRandomValueFromSeed } from "@arcanahq/core/assembly/primitives/random";
17
+
18
+ /**
19
+ * Base class for deck configuration
20
+ * Provides a flexible interface for any deck configuration
21
+ * Subclasses must implement fromIndex, toIndex, and totalCards
22
+ */
23
+ export class DeckConfig {
24
+ /**
25
+ * Get total number of cards in the deck
26
+ */
27
+ get totalCards(): i32 {
28
+ return 0; // Must be overridden
29
+ }
30
+
31
+ /**
32
+ * Convert an index (0 to totalCards-1) to a Card
33
+ * @param index The card index
34
+ * @returns The Card at that index, or null if invalid
35
+ */
36
+ fromIndex(index: i32): Card | null {
37
+ return null; // Must be overridden
38
+ }
39
+
40
+ /**
41
+ * Convert a Card to its index (0 to totalCards-1)
42
+ * @param card The card to convert
43
+ * @returns The index of the card, or -1 if not in deck
44
+ */
45
+ toIndex(card: Card): i32 {
46
+ return -1; // Must be overridden
47
+ }
48
+
49
+ // Backward compatibility properties
50
+ /**
51
+ * @deprecated Use totalCards instead
52
+ */
53
+ get deckSize(): i32 {
54
+ return this.totalCards;
55
+ }
56
+
57
+ /**
58
+ * Standard 52-card deck
59
+ */
60
+ static standard(): StandardDeckConfig {
61
+ return new StandardDeckConfig();
62
+ }
63
+
64
+ /**
65
+ * Spanish 21 deck (48 cards, no 10s)
66
+ */
67
+ static spanish21(): Spanish21DeckConfig {
68
+ return new Spanish21DeckConfig();
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Standard 52-card deck configuration
74
+ * Includes all ranks (2-A) in all suits
75
+ */
76
+ export class StandardDeckConfig extends DeckConfig {
77
+ private static readonly TOTAL_CARDS: i32 = 52;
78
+ private static readonly RANKS_PER_SUIT: i32 = 13;
79
+
80
+ get totalCards(): i32 {
81
+ return StandardDeckConfig.TOTAL_CARDS;
82
+ }
83
+
84
+ fromIndex(index: i32): Card | null {
85
+ if (index < 0 || index >= StandardDeckConfig.TOTAL_CARDS) {
86
+ return null;
87
+ }
88
+
89
+ const suitIndex = index / StandardDeckConfig.RANKS_PER_SUIT;
90
+ const rankIndex = index % StandardDeckConfig.RANKS_PER_SUIT;
91
+
92
+ if (suitIndex >= Suit.ALL.length || rankIndex >= Rank.ALL.length) {
93
+ return null;
94
+ }
95
+
96
+ return new Card(Suit.ALL[suitIndex], Rank.ALL[rankIndex]);
97
+ }
98
+
99
+ toIndex(card: Card): i32 {
100
+ // Find suit index
101
+ let suitIndex: i32 = -1;
102
+ for (let i = 0; i < Suit.ALL.length; i++) {
103
+ if (Suit.ALL[i] === card.suit) {
104
+ suitIndex = i;
105
+ break;
106
+ }
107
+ }
108
+
109
+ if (suitIndex < 0) {
110
+ return -1;
111
+ }
112
+
113
+ // Find rank index
114
+ let rankIndex: i32 = -1;
115
+ for (let i = 0; i < Rank.ALL.length; i++) {
116
+ if (Rank.ALL[i] === card.rank) {
117
+ rankIndex = i;
118
+ break;
119
+ }
120
+ }
121
+
122
+ if (rankIndex < 0) {
123
+ return -1;
124
+ }
125
+
126
+ // Standard deck: suitIndex * 13 + rankIndex
127
+ return suitIndex * StandardDeckConfig.RANKS_PER_SUIT + rankIndex;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Spanish 21 deck configuration (48 cards, no 10s)
133
+ * Excludes all 10s from the deck
134
+ */
135
+ export class Spanish21DeckConfig extends DeckConfig {
136
+ private static readonly TOTAL_CARDS: i32 = 48;
137
+ private static readonly RANKS_PER_SUIT: i32 = 12; // 13 ranks - 1 (no 10s)
138
+
139
+ get totalCards(): i32 {
140
+ return Spanish21DeckConfig.TOTAL_CARDS;
141
+ }
142
+
143
+ fromIndex(index: i32): Card | null {
144
+ if (index < 0 || index >= Spanish21DeckConfig.TOTAL_CARDS) {
145
+ return null;
146
+ }
147
+
148
+ const suitIndex = index / Spanish21DeckConfig.RANKS_PER_SUIT;
149
+ const rankIndex = index % Spanish21DeckConfig.RANKS_PER_SUIT;
150
+
151
+ if (suitIndex >= Suit.ALL.length) {
152
+ return null;
153
+ }
154
+
155
+ // Map rank index to actual rank (skip 10)
156
+ // Rank.ALL = [2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, K, A]
157
+ // Spanish 21 = [2, 3, 4, 5, 6, 7, 8, 9, J, Q, K, A] (no 10)
158
+ let actualRankIndex: i32;
159
+ if (rankIndex < 8) {
160
+ // 2-9 map directly
161
+ actualRankIndex = rankIndex;
162
+ } else {
163
+ // J, Q, K, A (indices 9, 10, 11, 12 in Rank.ALL)
164
+ actualRankIndex = rankIndex + 1; // Skip 10
165
+ }
166
+
167
+ if (actualRankIndex >= Rank.ALL.length) {
168
+ return null;
169
+ }
170
+
171
+ return new Card(Suit.ALL[suitIndex], Rank.ALL[actualRankIndex]);
172
+ }
173
+
174
+ toIndex(card: Card): i32 {
175
+ // Check if rank is 10 (not allowed in Spanish 21)
176
+ if (card.rank == Rank.TEN) {
177
+ return -1; // 10s not in Spanish 21 deck
178
+ }
179
+
180
+ // Find suit index
181
+ let suitIndex: i32 = -1;
182
+ for (let i = 0; i < Suit.ALL.length; i++) {
183
+ if (Suit.ALL[i] === card.suit) {
184
+ suitIndex = i;
185
+ break;
186
+ }
187
+ }
188
+
189
+ if (suitIndex < 0) {
190
+ return -1;
191
+ }
192
+
193
+ // Find rank index
194
+ let rankIndex: i32 = -1;
195
+ for (let i = 0; i < Rank.ALL.length; i++) {
196
+ if (Rank.ALL[i] === card.rank) {
197
+ rankIndex = i;
198
+ break;
199
+ }
200
+ }
201
+
202
+ if (rankIndex < 0) {
203
+ return -1;
204
+ }
205
+
206
+ // Map rank index to Spanish 21 index (skip 10)
207
+ let spanishRankIndex: i32;
208
+ if (rankIndex < 8) {
209
+ // 2-9 map directly
210
+ spanishRankIndex = rankIndex;
211
+ } else if (rankIndex == 8) {
212
+ // 10 is not in Spanish 21
213
+ return -1;
214
+ } else {
215
+ // J, Q, K, A (indices 9, 10, 11, 12) map to 8, 9, 10, 11
216
+ spanishRankIndex = rankIndex - 1;
217
+ }
218
+
219
+ return suitIndex * Spanish21DeckConfig.RANKS_PER_SUIT + spanishRankIndex;
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Factory functions for creating deck configurations
225
+ * These are static methods on the DeckConfig class
226
+ */
227
+ // Note: Factory methods are defined below after the concrete classes
228
+
229
+ /**
230
+ * Shoe configuration for multi-deck games
231
+ * A shoe contains multiple decks shuffled together
232
+ * Supports any number of decks (1, 2, 4, 6, 8, etc.)
233
+ */
234
+ export class ShoeConfig {
235
+ deckConfig: DeckConfig;
236
+ numDecks: i32 = 1; // Number of decks in the shoe (can be any positive integer)
237
+
238
+ /**
239
+ * Create a shoe configuration with any number of decks
240
+ * @param deckConfig The deck configuration (standard or Spanish 21)
241
+ * @param numDecks Number of decks in the shoe (must be >= 1)
242
+ */
243
+ constructor(deckConfig: DeckConfig, numDecks: i32 = 1) {
244
+ if (numDecks < 1) {
245
+ throw new Error("Number of decks must be at least 1");
246
+ }
247
+ this.deckConfig = deckConfig;
248
+ this.numDecks = numDecks;
249
+ }
250
+
251
+ /**
252
+ * Get total number of cards in the shoe
253
+ */
254
+ getShoeSize(): i32 {
255
+ return this.deckConfig.totalCards * this.numDecks;
256
+ }
257
+
258
+ /**
259
+ * Create a shoe with any number of standard decks
260
+ * @param numDecks Number of decks (1, 2, 4, 6, 8, etc.)
261
+ */
262
+ static standard(numDecks: i32 = 1): ShoeConfig {
263
+ return new ShoeConfig(DeckConfig.standard(), numDecks);
264
+ }
265
+
266
+ /**
267
+ * Create a shoe with any number of Spanish 21 decks
268
+ * @param numDecks Number of decks (1, 2, 4, 6, 8, etc.)
269
+ */
270
+ static spanish21(numDecks: i32 = 1): ShoeConfig {
271
+ return new ShoeConfig(DeckConfig.spanish21(), numDecks);
272
+ }
273
+
274
+ /**
275
+ * Create a shoe with a custom deck configuration and any number of decks
276
+ * This is the most flexible method - you can use any DeckConfig
277
+ *
278
+ * @param deckConfig Custom deck configuration (e.g., DeckConfig.spanish21(), DeckConfig.standard(), or a custom one)
279
+ * @param numDecks Number of decks (1, 2, 4, 6, 8, etc.)
280
+ *
281
+ * @example
282
+ * // Spanish 21 shoe with 6 decks
283
+ * const shoe = ShoeConfig.withDeck(DeckConfig.spanish21(), 6);
284
+ *
285
+ * // Standard deck shoe with 8 decks
286
+ * const shoe = ShoeConfig.withDeck(DeckConfig.standard(), 8);
287
+ *
288
+ * // Custom deck configuration
289
+ * const customDeck = new DeckConfig(52, true);
290
+ * const shoe = ShoeConfig.withDeck(customDeck, 4);
291
+ */
292
+ static withDeck(deckConfig: DeckConfig, numDecks: i32 = 1): ShoeConfig {
293
+ return new ShoeConfig(deckConfig, numDecks);
294
+ }
295
+
296
+ /**
297
+ * Create a shoe with custom deck configuration and any number of decks
298
+ * @deprecated Use ShoeConfig.withDeck() instead for better clarity
299
+ * @param deckConfig Custom deck configuration
300
+ * @param numDecks Number of decks (1, 2, 4, 6, 8, etc.)
301
+ */
302
+ static custom(deckConfig: DeckConfig, numDecks: i32 = 1): ShoeConfig {
303
+ return new ShoeConfig(deckConfig, numDecks);
304
+ }
305
+
306
+ // Convenience methods for common configurations (backward compatibility)
307
+
308
+ /**
309
+ * Single standard deck (52 cards)
310
+ * @deprecated Use ShoeConfig.standard(1) instead
311
+ */
312
+ static singleDeck(): ShoeConfig {
313
+ return new ShoeConfig(DeckConfig.standard(), 1);
314
+ }
315
+
316
+ /**
317
+ * 6-deck shoe (312 cards)
318
+ * @deprecated Use ShoeConfig.standard(6) instead
319
+ */
320
+ static sixDeck(): ShoeConfig {
321
+ return new ShoeConfig(DeckConfig.standard(), 6);
322
+ }
323
+
324
+ /**
325
+ * 8-deck shoe (416 cards)
326
+ * @deprecated Use ShoeConfig.standard(8) instead
327
+ */
328
+ static eightDeck(): ShoeConfig {
329
+ return new ShoeConfig(DeckConfig.standard(), 8);
330
+ }
331
+
332
+ /**
333
+ * Spanish 21 single deck (48 cards)
334
+ * @deprecated Use ShoeConfig.spanish21(1) instead
335
+ */
336
+ static spanish21SingleDeck(): ShoeConfig {
337
+ return new ShoeConfig(DeckConfig.spanish21(), 1);
338
+ }
339
+
340
+ /**
341
+ * Spanish 21 6-deck shoe (288 cards)
342
+ * @deprecated Use ShoeConfig.spanish21(6) instead
343
+ */
344
+ static spanish21SixDeck(): ShoeConfig {
345
+ return new ShoeConfig(DeckConfig.spanish21(), 6);
346
+ }
347
+
348
+ /**
349
+ * Spanish 21 8-deck shoe (384 cards)
350
+ * @deprecated Use ShoeConfig.spanish21(8) instead
351
+ */
352
+ static spanish21EightDeck(): ShoeConfig {
353
+ return new ShoeConfig(DeckConfig.spanish21(), 8);
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Card index mapping utilities
359
+ *
360
+ * @deprecated Use DeckConfig.fromIndex() and DeckConfig.toIndex() instead
361
+ * This class is kept for backward compatibility
362
+ */
363
+ export class CardIndexMapper {
364
+ /**
365
+ * Convert card index to Card object
366
+ * @deprecated Use config.fromIndex(index) instead
367
+ */
368
+ static indexToCard(index: i32, config: DeckConfig): Card | null {
369
+ return config.fromIndex(index);
370
+ }
371
+
372
+ /**
373
+ * Convert Card object to index
374
+ * @deprecated Use config.toIndex(card) instead
375
+ */
376
+ static cardToIndex(card: Card, config: DeckConfig): i32 {
377
+ return config.toIndex(card);
378
+ }
379
+
380
+ /**
381
+ * Create unshuffled shoe indices for a multi-deck shoe
382
+ * Combines multiple decks into a single shoe
383
+ */
384
+ static createUnshuffledShoeIndices(shoeConfig: ShoeConfig): i32[] {
385
+ const deckSize = shoeConfig.deckConfig.totalCards;
386
+ const numDecks = shoeConfig.numDecks;
387
+ const shoeSize = deckSize * numDecks;
388
+
389
+ const shoeIndices = new Array<i32>(shoeSize);
390
+
391
+ // For each deck in the shoe
392
+ for (let deckNum = 0; deckNum < numDecks; deckNum++) {
393
+ // Create unshuffled deck indices for this deck
394
+ const deckIndices = CardIndexMapper.createUnshuffledDeckIndices(shoeConfig.deckConfig);
395
+
396
+ // Add deck indices to shoe (each deck has the same card order)
397
+ for (let i = 0; i < deckSize; i++) {
398
+ shoeIndices[deckNum * deckSize + i] = deckIndices[i];
399
+ }
400
+ }
401
+
402
+ return shoeIndices;
403
+ }
404
+
405
+ /**
406
+ * Create unshuffled deck as array of indices
407
+ * Uses the deck config's toIndex method to generate indices
408
+ */
409
+ static createUnshuffledDeckIndices(config: DeckConfig): i32[] {
410
+ const totalCards = config.totalCards;
411
+ const indices = new Array<i32>(totalCards);
412
+
413
+ // Generate all valid cards and map them to indices
414
+ // For standard deck: iterate through all suits and ranks
415
+ // For Spanish 21: iterate through all suits and ranks except 10s
416
+
417
+ // Generic approach: try all possible cards and use toIndex
418
+ // This works for any deck configuration
419
+ let index = 0;
420
+ for (let s = 0; s < Suit.ALL.length; s++) {
421
+ for (let r = 0; r < Rank.ALL.length; r++) {
422
+ const card = new Card(Suit.ALL[s], Rank.ALL[r]);
423
+ const cardIndex = config.toIndex(card);
424
+ if (cardIndex >= 0 && cardIndex < totalCards) {
425
+ indices[index] = cardIndex;
426
+ index++;
427
+ if (index >= totalCards) {
428
+ break;
429
+ }
430
+ }
431
+ }
432
+ if (index >= totalCards) {
433
+ break;
434
+ }
435
+ }
436
+
437
+ return indices;
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Deterministic shuffle using Fisher-Yates algorithm
443
+ * Shuffles an array of card indices
444
+ */
445
+ export function deterministicShuffleIndices(
446
+ indices: i32[],
447
+ shuffleId: string,
448
+ shuffleSalt: string,
449
+ seedIndex: i32
450
+ ): i32[] {
451
+ const shuffled = new Array<i32>(indices.length);
452
+ // Copy indices
453
+ for (let i = 0; i < indices.length; i++) {
454
+ shuffled[i] = indices[i];
455
+ }
456
+
457
+ // Create seed from shuffleId, salt, and seedIndex
458
+ const seed = shuffleId + ":" + shuffleSalt + ":" + seedIndex.toString();
459
+
460
+ // Fisher-Yates shuffle with deterministic random
461
+ for (let i = shuffled.length - 1; i > 0; i--) {
462
+ // Get deterministic random value
463
+ const randomResult = getRandomValueFromSeed(seed, i);
464
+ const randomValue = randomResult.value;
465
+
466
+ // Map [0, 1) to [0, i+1)
467
+ const j = <i32>(randomValue * <f64>(i + 1));
468
+
469
+ // Swap
470
+ const temp = shuffled[i];
471
+ shuffled[i] = shuffled[j];
472
+ shuffled[j] = temp;
473
+ }
474
+
475
+ return shuffled;
476
+ }
477
+
478
+ /**
479
+ * Deal a card by index from a deterministically shuffled deck
480
+ * Returns the Card object at the specified position
481
+ *
482
+ * @deprecated Use dealCardFromShoe for multi-deck support
483
+ */
484
+ export function dealCardByIndex(
485
+ shuffleId: string,
486
+ shuffleSalt: string,
487
+ dealtCardCount: i32,
488
+ config: DeckConfig
489
+ ): Card {
490
+ // Create unshuffled deck indices
491
+ const unshuffledIndices = CardIndexMapper.createUnshuffledDeckIndices(config);
492
+
493
+ // Determine which shuffle iteration we're on
494
+ const shuffleIteration = dealtCardCount / config.totalCards;
495
+ const cardIndexInShuffle = dealtCardCount % config.totalCards;
496
+
497
+ // Shuffle deterministically
498
+ const shuffledIndices = deterministicShuffleIndices(
499
+ unshuffledIndices,
500
+ shuffleId,
501
+ shuffleSalt,
502
+ shuffleIteration
503
+ );
504
+
505
+ // Get the card index at the position
506
+ const cardIndex = shuffledIndices[cardIndexInShuffle];
507
+
508
+ // Convert index to Card using deck config
509
+ const card = config.fromIndex(cardIndex);
510
+ if (card === null) {
511
+ // Fallback: return a default card (shouldn't happen)
512
+ return new Card(Suit.SPADES, Rank.TWO);
513
+ }
514
+
515
+ return card;
516
+ }
517
+
518
+ /**
519
+ * Deal a card from a multi-deck shoe
520
+ * Handles shoe exhaustion and reshuffling automatically
521
+ *
522
+ * @param shuffleId Unique identifier for the shuffle
523
+ * @param shuffleSalt Secret salt for shuffle verification
524
+ * @param shoePosition Current position in the shoe (0 to shoeSize-1)
525
+ * @param shoeConfig Shoe configuration (number of decks, deck type)
526
+ * @returns The card at the current shoe position
527
+ */
528
+ export function dealCardFromShoe(
529
+ shuffleId: string,
530
+ shuffleSalt: string,
531
+ shoePosition: i32,
532
+ shoeConfig: ShoeConfig
533
+ ): Card {
534
+ const shoeSize = shoeConfig.getShoeSize();
535
+
536
+ // If we've exhausted the shoe, reshuffle
537
+ // Determine which shoe iteration we're on
538
+ const shoeIteration = shoePosition / shoeSize;
539
+ const positionInShoe = shoePosition % shoeSize;
540
+
541
+ // Create unshuffled shoe indices (all decks combined)
542
+ const unshuffledShoeIndices = CardIndexMapper.createUnshuffledShoeIndices(shoeConfig);
543
+
544
+ // Shuffle the entire shoe deterministically
545
+ const shuffledShoeIndices = deterministicShuffleIndices(
546
+ unshuffledShoeIndices,
547
+ shuffleId,
548
+ shuffleSalt,
549
+ shoeIteration
550
+ );
551
+
552
+ // Get the card index at the position in the current shoe
553
+ const cardIndex = shuffledShoeIndices[positionInShoe];
554
+
555
+ // Convert index to Card using deck config
556
+ const card = shoeConfig.deckConfig.fromIndex(cardIndex);
557
+ if (card === null) {
558
+ // Fallback: return a default card (shouldn't happen)
559
+ return new Card(Suit.SPADES, Rank.TWO);
560
+ }
561
+
562
+ return card;
563
+ }
564
+
565
+ /**
566
+ * Get shuffled shoe indices for one shoe block (one shuffle).
567
+ * Efficient: call once per batch of cards from the shoe, then index into the returned array.
568
+ *
569
+ * @param shuffleId Unique identifier for the shuffle
570
+ * @param shuffleSalt Salt for deterministic shuffle
571
+ * @param shoeIteration Which shoe block (0 = first shoe, 1 = next, etc.)
572
+ * @param shoeConfig Shoe configuration
573
+ * @returns Shuffled indices for the shoe; card at position i is shoeConfig.deckConfig.fromIndex(result[i])
574
+ */
575
+ export function getShuffledShoeIndices(
576
+ shuffleId: string,
577
+ shuffleSalt: string,
578
+ shoeIteration: i32,
579
+ shoeConfig: ShoeConfig
580
+ ): i32[] {
581
+ const unshuffledShoeIndices = CardIndexMapper.createUnshuffledShoeIndices(shoeConfig);
582
+ return deterministicShuffleIndices(
583
+ unshuffledShoeIndices,
584
+ shuffleId,
585
+ shuffleSalt,
586
+ shoeIteration
587
+ );
588
+ }
589
+
590
+ /**
591
+ * Deal multiple cards from a shoe with at most one or two shuffles (two only when crossing the shoe boundary).
592
+ * Use this instead of calling dealCardFromShoe in a loop to stay within WASM instruction limits.
593
+ *
594
+ * @param shuffleId Unique identifier for the shuffle
595
+ * @param shuffleSalt Salt for deterministic shuffle
596
+ * @param shoePosition Current position in the shoe (number of cards already dealt)
597
+ * @param count Number of cards to deal
598
+ * @param shoeConfig Shoe configuration
599
+ * @returns Array of count cards; caller must advance shoePosition by count
600
+ */
601
+ export function dealCardsFromShoe(
602
+ shuffleId: string,
603
+ shuffleSalt: string,
604
+ shoePosition: i32,
605
+ count: i32,
606
+ shoeConfig: ShoeConfig
607
+ ): Card[] {
608
+ const result = new Array<Card>();
609
+ if (count <= 0) return result;
610
+ const shoeSize = shoeConfig.getShoeSize();
611
+ const shoeIteration = shoePosition / shoeSize;
612
+ const base = shoePosition % shoeSize;
613
+ const indices0 = getShuffledShoeIndices(shuffleId, shuffleSalt, shoeIteration, shoeConfig);
614
+ let indices1: i32[] = [];
615
+ if (base + count > shoeSize) {
616
+ indices1 = getShuffledShoeIndices(shuffleId, shuffleSalt, shoeIteration + 1, shoeConfig);
617
+ }
618
+ const config = shoeConfig.deckConfig;
619
+ for (let i = 0; i < count; i++) {
620
+ const idx = base + i;
621
+ const cardIndex = idx < shoeSize ? indices0[idx] : indices1[idx - shoeSize];
622
+ const card = config.fromIndex(cardIndex);
623
+ if (card !== null) {
624
+ result.push(card);
625
+ } else {
626
+ result.push(new Card(Suit.SPADES, Rank.TWO));
627
+ }
628
+ }
629
+ return result;
630
+ }
631
+
632
+ /**
633
+ * Create a full shuffled deck as Card objects
634
+ * Useful for testing or when you need the entire deck
635
+ */
636
+ export function createShuffledDeck(
637
+ shuffleId: string,
638
+ shuffleSalt: string,
639
+ seedIndex: i32,
640
+ config: DeckConfig
641
+ ): Card[] {
642
+ const unshuffledIndices = CardIndexMapper.createUnshuffledDeckIndices(config);
643
+ const shuffledIndices = deterministicShuffleIndices(
644
+ unshuffledIndices,
645
+ shuffleId,
646
+ shuffleSalt,
647
+ seedIndex
648
+ );
649
+
650
+ const deck = new Array<Card>(config.totalCards);
651
+ for (let i = 0; i < config.totalCards; i++) {
652
+ const card = config.fromIndex(shuffledIndices[i]);
653
+ if (card !== null) {
654
+ deck[i] = card;
655
+ } else {
656
+ // Fallback
657
+ deck[i] = new Card(Suit.SPADES, Rank.TWO);
658
+ }
659
+ }
660
+
661
+ return deck;
662
+ }
663
+
664
+ /**
665
+ * Get shuffled indices for one deck block (one shuffle).
666
+ * Efficient: call once per batch of cards, then index into the returned array.
667
+ * Use when dealing multiple cards in the same action to avoid instruction limit.
668
+ *
669
+ * @param shuffleId Unique identifier for the shuffle
670
+ * @param shuffleSalt Salt for deterministic shuffle
671
+ * @param seedIndex Which 52-card block (0 = first deck, 1 = next deck, etc.)
672
+ * @param config Deck configuration
673
+ * @returns Shuffled indices; card at position i is config.fromIndex(result[i])
674
+ */
675
+ export function getShuffledIndices(
676
+ shuffleId: string,
677
+ shuffleSalt: string,
678
+ seedIndex: i32,
679
+ config: DeckConfig
680
+ ): i32[] {
681
+ const unshuffledIndices = CardIndexMapper.createUnshuffledDeckIndices(config);
682
+ return deterministicShuffleIndices(unshuffledIndices, shuffleId, shuffleSalt, seedIndex);
683
+ }
684
+
685
+ /**
686
+ * Deal multiple cards with at most one or two shuffles (two only when crossing the deck boundary).
687
+ * Use this instead of calling dealCardByIndex in a loop to stay within WASM instruction limits.
688
+ *
689
+ * @param shuffleId Unique identifier for the shuffle
690
+ * @param shuffleSalt Salt for deterministic shuffle
691
+ * @param dealtCardCount Current position (number of cards already dealt)
692
+ * @param count Number of cards to deal
693
+ * @param config Deck configuration
694
+ * @returns Array of count cards; caller must advance dealtCardCount by count
695
+ */
696
+ export function dealCards(
697
+ shuffleId: string,
698
+ shuffleSalt: string,
699
+ dealtCardCount: i32,
700
+ count: i32,
701
+ config: DeckConfig
702
+ ): Card[] {
703
+ const result = new Array<Card>();
704
+ if (count <= 0) return result;
705
+ const deckSize = config.totalCards;
706
+ const seedIndex0 = dealtCardCount / deckSize;
707
+ const base = dealtCardCount % deckSize;
708
+ const indices0 = getShuffledIndices(shuffleId, shuffleSalt, seedIndex0, config);
709
+ let indices1: i32[] = [];
710
+ if (base + count > deckSize) {
711
+ indices1 = getShuffledIndices(shuffleId, shuffleSalt, seedIndex0 + 1, config);
712
+ }
713
+ for (let i = 0; i < count; i++) {
714
+ const idx = base + i;
715
+ const cardIndex = idx < deckSize ? indices0[idx] : indices1[idx - deckSize];
716
+ const card = config.fromIndex(cardIndex);
717
+ if (card !== null) {
718
+ result.push(card);
719
+ } else {
720
+ result.push(new Card(Suit.SPADES, Rank.TWO));
721
+ }
722
+ }
723
+ return result;
724
+ }
725
+
726
+ /**
727
+ * Create unshuffled deck as Card objects
728
+ */
729
+ export function createUnshuffledDeck(config: DeckConfig): Card[] {
730
+ const indices = CardIndexMapper.createUnshuffledDeckIndices(config);
731
+ const deck = new Array<Card>(config.totalCards);
732
+
733
+ for (let i = 0; i < config.totalCards; i++) {
734
+ const card = config.fromIndex(indices[i]);
735
+ if (card !== null) {
736
+ deck[i] = card;
737
+ } else {
738
+ // Fallback
739
+ deck[i] = new Card(Suit.SPADES, Rank.TWO);
740
+ }
741
+ }
742
+
743
+ return deck;
744
+ }