@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,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
|
+
}
|