@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,314 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Card Game Utilities Library
|
|
4
|
+
*
|
|
5
|
+
* Provides common card game operations that can be shared across different card games:
|
|
6
|
+
* - Standard deck creation (52 cards)
|
|
7
|
+
* - Deterministic shuffling using Fisher-Yates algorithm
|
|
8
|
+
* - Card dealing utilities
|
|
9
|
+
*
|
|
10
|
+
* All operations are deterministic when using seeded randomness from the random module.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Card, Suit, Rank } from "./cards";
|
|
14
|
+
import { RandomSeed, getRandomIntInRange } from "@arcanahq/core/assembly/primitives/random";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates a standard 52-card deck in unshuffled order
|
|
18
|
+
* Cards are ordered: suits in order (Spades, Hearts, Diamonds, Clubs),
|
|
19
|
+
* ranks in order (2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, K, A)
|
|
20
|
+
*
|
|
21
|
+
* @returns A new array containing all 52 cards in standard order
|
|
22
|
+
*/
|
|
23
|
+
export function createStandardDeck(): Card[] {
|
|
24
|
+
const deck = new Array<Card>(52);
|
|
25
|
+
let index = 0;
|
|
26
|
+
|
|
27
|
+
for (let s = 0; s < Suit.ALL.length; s++) {
|
|
28
|
+
for (let r = 0; r < Rank.ALL.length; r++) {
|
|
29
|
+
deck[index] = new Card(Suit.ALL[s], Rank.ALL[r]);
|
|
30
|
+
index++;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return deck;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Shuffles a deck using the Fisher-Yates algorithm with deterministic randomness
|
|
39
|
+
* The shuffle is reproducible given the same seed
|
|
40
|
+
*
|
|
41
|
+
* @param deck The deck to shuffle (will be modified in place)
|
|
42
|
+
* @param seed The random seed to use for shuffling
|
|
43
|
+
* @returns The updated random seed after shuffling
|
|
44
|
+
*/
|
|
45
|
+
export function shuffleDeck(deck: Card[], seed: RandomSeed): RandomSeed {
|
|
46
|
+
const n = deck.length;
|
|
47
|
+
let currentSeed = seed;
|
|
48
|
+
|
|
49
|
+
// Fisher-Yates shuffle: iterate from last element to first
|
|
50
|
+
for (let i = n - 1; i > 0; i--) {
|
|
51
|
+
// Get random index from 0 to i (inclusive)
|
|
52
|
+
const result = getRandomIntInRange(currentSeed, 0, i + 1);
|
|
53
|
+
const j = <i32>result.value;
|
|
54
|
+
currentSeed = result.seed;
|
|
55
|
+
|
|
56
|
+
// Swap cards[i] and cards[j]
|
|
57
|
+
const temp = deck[i];
|
|
58
|
+
deck[i] = deck[j];
|
|
59
|
+
deck[j] = temp;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return currentSeed;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Result class for createShuffledDeck
|
|
67
|
+
*/
|
|
68
|
+
export class ShuffledDeckResult {
|
|
69
|
+
deck: Card[];
|
|
70
|
+
newSeed: RandomSeed;
|
|
71
|
+
|
|
72
|
+
constructor(deck: Card[], newSeed: RandomSeed) {
|
|
73
|
+
this.deck = deck;
|
|
74
|
+
this.newSeed = newSeed;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Creates a shuffled deck from a standard deck using deterministic randomness
|
|
80
|
+
*
|
|
81
|
+
* @param seed The random seed to use for shuffling
|
|
82
|
+
* @returns A ShuffledDeckResult containing the shuffled deck and the updated random seed
|
|
83
|
+
*/
|
|
84
|
+
export function createShuffledDeck(seed: RandomSeed): ShuffledDeckResult {
|
|
85
|
+
const deck = createStandardDeck();
|
|
86
|
+
const newSeed = shuffleDeck(deck, seed);
|
|
87
|
+
return new ShuffledDeckResult(deck, newSeed);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Deals a single card from the top of the deck
|
|
92
|
+
*
|
|
93
|
+
* @param deck The deck to deal from (will be modified)
|
|
94
|
+
* @returns The dealt card, or null if the deck is empty
|
|
95
|
+
*/
|
|
96
|
+
export function dealCard(deck: Card[]): Card | null {
|
|
97
|
+
if (deck.length === 0) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const card = deck.pop();
|
|
101
|
+
return card !== null ? card : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Deals multiple cards from the top of the deck
|
|
106
|
+
*
|
|
107
|
+
* @param deck The deck to deal from (will be modified)
|
|
108
|
+
* @param count The number of cards to deal
|
|
109
|
+
* @returns An array of dealt cards (may be shorter than count if deck runs out)
|
|
110
|
+
*/
|
|
111
|
+
export function dealCards(deck: Card[], count: i32): Card[] {
|
|
112
|
+
const dealt = new Array<Card>(0);
|
|
113
|
+
for (let i = 0; i < count; i++) {
|
|
114
|
+
if (deck.length === 0) {
|
|
115
|
+
break; // Deck is empty
|
|
116
|
+
}
|
|
117
|
+
const card = dealCard(deck);
|
|
118
|
+
if (card !== null) {
|
|
119
|
+
dealt.push(card);
|
|
120
|
+
} else {
|
|
121
|
+
break; // Deck is empty
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return dealt;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Result class for dealCardFromDeck
|
|
129
|
+
*/
|
|
130
|
+
export class DealCardResult {
|
|
131
|
+
card: Card | null;
|
|
132
|
+
newDeck: Card[];
|
|
133
|
+
newSeed: RandomSeed;
|
|
134
|
+
|
|
135
|
+
constructor(card: Card | null, newDeck: Card[], newSeed: RandomSeed) {
|
|
136
|
+
this.card = card;
|
|
137
|
+
this.newDeck = newDeck;
|
|
138
|
+
this.newSeed = newSeed;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Deals a card from a specific position in the deck using deterministic randomness
|
|
144
|
+
* This is useful for games that need to deal cards in a specific order based on a seed
|
|
145
|
+
*
|
|
146
|
+
* @param deck The deck to deal from
|
|
147
|
+
* @param seed The random seed to use for selecting the card
|
|
148
|
+
* @returns A DealCardResult containing the dealt card, the updated deck, and the updated seed
|
|
149
|
+
*/
|
|
150
|
+
export function dealCardFromDeck(
|
|
151
|
+
deck: Card[],
|
|
152
|
+
seed: RandomSeed
|
|
153
|
+
): DealCardResult {
|
|
154
|
+
if (deck.length === 0) {
|
|
155
|
+
return new DealCardResult(null, deck, seed);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Use deterministic randomness to select a card index
|
|
159
|
+
const result = getRandomIntInRange(seed, 0, deck.length);
|
|
160
|
+
const cardIndex = <i32>result.value;
|
|
161
|
+
const newSeed = result.seed;
|
|
162
|
+
|
|
163
|
+
// Remove the selected card from the deck
|
|
164
|
+
const card = deck[cardIndex];
|
|
165
|
+
const newDeck = new Array<Card>(deck.length - 1);
|
|
166
|
+
|
|
167
|
+
let newDeckIndex = 0;
|
|
168
|
+
for (let i = 0; i < deck.length; i++) {
|
|
169
|
+
if (i !== cardIndex) {
|
|
170
|
+
newDeck[newDeckIndex] = deck[i];
|
|
171
|
+
newDeckIndex++;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return new DealCardResult(card, newDeck, newSeed);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Gets the number of cards remaining in the deck
|
|
180
|
+
*
|
|
181
|
+
* @param deck The deck to check
|
|
182
|
+
* @returns The number of cards in the deck
|
|
183
|
+
*/
|
|
184
|
+
export function getDeckSize(deck: Card[]): i32 {
|
|
185
|
+
return deck.length;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Checks if the deck is empty
|
|
190
|
+
*
|
|
191
|
+
* @param deck The deck to check
|
|
192
|
+
* @returns True if the deck is empty, false otherwise
|
|
193
|
+
*/
|
|
194
|
+
export function isDeckEmpty(deck: Card[]): bool {
|
|
195
|
+
return deck.length === 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Creates a copy of a deck without modifying the original
|
|
200
|
+
*
|
|
201
|
+
* @param deck The deck to clone
|
|
202
|
+
* @returns A new array containing copies of all cards in the deck
|
|
203
|
+
*/
|
|
204
|
+
export function cloneDeck(deck: Card[]): Card[] {
|
|
205
|
+
const cloned = new Array<Card>(deck.length);
|
|
206
|
+
for (let i = 0; i < deck.length; i++) {
|
|
207
|
+
cloned[i] = deck[i];
|
|
208
|
+
}
|
|
209
|
+
return cloned;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Converts a Card to an integer representation (0-51)
|
|
214
|
+
* Mapping: cardIndex = suitIndex * 13 + rankIndex
|
|
215
|
+
* - Suit order: Spades=0, Hearts=1, Diamonds=2, Clubs=3
|
|
216
|
+
* - Rank order: 2=0, 3=1, 4=2, 5=3, 6=4, 7=5, 8=6, 9=7, 10=8, J=9, Q=10, K=11, A=12
|
|
217
|
+
*
|
|
218
|
+
* @param card The card to convert
|
|
219
|
+
* @returns An integer from 0-51 representing the card
|
|
220
|
+
*/
|
|
221
|
+
export function cardToInt(card: Card): i32 {
|
|
222
|
+
// Find suit index
|
|
223
|
+
let suitIndex: i32 = -1;
|
|
224
|
+
for (let i = 0; i < Suit.ALL.length; i++) {
|
|
225
|
+
if (Suit.ALL[i] === card.suit) {
|
|
226
|
+
suitIndex = i;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Find rank index
|
|
232
|
+
let rankIndex: i32 = -1;
|
|
233
|
+
for (let i = 0; i < Rank.ALL.length; i++) {
|
|
234
|
+
if (Rank.ALL[i] === card.rank) {
|
|
235
|
+
rankIndex = i;
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// If card is invalid, return -1
|
|
241
|
+
if (suitIndex < 0 || rankIndex < 0) {
|
|
242
|
+
return -1;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Calculate card index: suitIndex * 13 + rankIndex
|
|
246
|
+
return suitIndex * 13 + rankIndex;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Converts an integer (0-51) back to a Card
|
|
251
|
+
* Mapping: cardIndex = suitIndex * 13 + rankIndex
|
|
252
|
+
* - Suit order: Spades=0, Hearts=1, Diamonds=2, Clubs=3
|
|
253
|
+
* - Rank order: 2=0, 3=1, 4=2, 5=3, 6=4, 7=5, 8=6, 9=7, 10=8, J=9, Q=10, K=11, A=12
|
|
254
|
+
*
|
|
255
|
+
* @param value The integer value (0-51) representing the card
|
|
256
|
+
* @returns A Card object, or null if the value is invalid
|
|
257
|
+
*/
|
|
258
|
+
export function intToCard(value: i32): Card | null {
|
|
259
|
+
// Validate range
|
|
260
|
+
if (value < 0 || value >= 52) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Calculate suit and rank indices
|
|
265
|
+
const suitIndex = value / 13; // Integer division
|
|
266
|
+
const rankIndex = value % 13; // Modulo
|
|
267
|
+
|
|
268
|
+
// Get suit and rank from arrays
|
|
269
|
+
if (suitIndex >= Suit.ALL.length || rankIndex >= Rank.ALL.length) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const suit = Suit.ALL[suitIndex];
|
|
274
|
+
const rank = Rank.ALL[rankIndex];
|
|
275
|
+
|
|
276
|
+
return new Card(suit, rank);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Converts a deck of cards to an array of integers
|
|
281
|
+
* Useful for serialization and storage
|
|
282
|
+
*
|
|
283
|
+
* @param deck The deck to serialize
|
|
284
|
+
* @returns An array of integers (0-51) representing each card
|
|
285
|
+
*/
|
|
286
|
+
export function deckToIntArray(deck: Card[]): i32[] {
|
|
287
|
+
const result = new Array<i32>(deck.length);
|
|
288
|
+
for (let i = 0; i < deck.length; i++) {
|
|
289
|
+
result[i] = cardToInt(deck[i]);
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Converts an array of integers back to a deck of cards
|
|
296
|
+
* Useful for deserialization
|
|
297
|
+
*
|
|
298
|
+
* @param ints An array of integers (0-51) representing cards
|
|
299
|
+
* @returns An array of Card objects
|
|
300
|
+
*/
|
|
301
|
+
export function intArrayToDeck(ints: i32[]): Card[] {
|
|
302
|
+
const deck = new Array<Card>(ints.length);
|
|
303
|
+
for (let i = 0; i < ints.length; i++) {
|
|
304
|
+
const card = intToCard(ints[i]);
|
|
305
|
+
if (card !== null) {
|
|
306
|
+
deck[i] = card;
|
|
307
|
+
} else {
|
|
308
|
+
// Invalid card value - skip or use a default?
|
|
309
|
+
// For now, we'll create a placeholder card
|
|
310
|
+
deck[i] = new Card(Suit.SPADES, Rank.TWO);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return deck;
|
|
314
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Deck of Cards Library
|
|
4
|
+
*
|
|
5
|
+
* Provides Card, Suit, and Rank classes for card games.
|
|
6
|
+
* All operations are deterministic when using seeded randomness.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { getRandomIntInRange, RandomSeed, RandomResult } from "@arcanahq/core/assembly/primitives/random";
|
|
10
|
+
|
|
11
|
+
// Card suit constants
|
|
12
|
+
export class Suit {
|
|
13
|
+
static readonly SPADES: string = "♠";
|
|
14
|
+
static readonly HEARTS: string = "♥";
|
|
15
|
+
static readonly DIAMONDS: string = "♦";
|
|
16
|
+
static readonly CLUBS: string = "♣";
|
|
17
|
+
|
|
18
|
+
static readonly ALL: string[] = [Suit.SPADES, Suit.HEARTS, Suit.DIAMONDS, Suit.CLUBS];
|
|
19
|
+
|
|
20
|
+
static isValid(suit: string): bool {
|
|
21
|
+
return suit === Suit.SPADES || suit === Suit.HEARTS || suit === Suit.DIAMONDS || suit === Suit.CLUBS;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Card rank constants
|
|
26
|
+
export class Rank {
|
|
27
|
+
static readonly TWO: string = "2";
|
|
28
|
+
static readonly THREE: string = "3";
|
|
29
|
+
static readonly FOUR: string = "4";
|
|
30
|
+
static readonly FIVE: string = "5";
|
|
31
|
+
static readonly SIX: string = "6";
|
|
32
|
+
static readonly SEVEN: string = "7";
|
|
33
|
+
static readonly EIGHT: string = "8";
|
|
34
|
+
static readonly NINE: string = "9";
|
|
35
|
+
static readonly TEN: string = "10";
|
|
36
|
+
static readonly JACK: string = "J";
|
|
37
|
+
static readonly QUEEN: string = "Q";
|
|
38
|
+
static readonly KING: string = "K";
|
|
39
|
+
static readonly ACE: string = "A";
|
|
40
|
+
|
|
41
|
+
static readonly ALL: string[] = [
|
|
42
|
+
Rank.TWO, Rank.THREE, Rank.FOUR, Rank.FIVE, Rank.SIX, Rank.SEVEN,
|
|
43
|
+
Rank.EIGHT, Rank.NINE, Rank.TEN, Rank.JACK, Rank.QUEEN, Rank.KING, Rank.ACE
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
static isValid(rank: string): bool {
|
|
47
|
+
for (let i = 0; i < Rank.ALL.length; i++) {
|
|
48
|
+
if (Rank.ALL[i] === rank) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get numeric value for comparison (2=2, A=14)
|
|
57
|
+
*/
|
|
58
|
+
static getValue(rank: string): i32 {
|
|
59
|
+
if (rank === Rank.TWO) return 2;
|
|
60
|
+
if (rank === Rank.THREE) return 3;
|
|
61
|
+
if (rank === Rank.FOUR) return 4;
|
|
62
|
+
if (rank === Rank.FIVE) return 5;
|
|
63
|
+
if (rank === Rank.SIX) return 6;
|
|
64
|
+
if (rank === Rank.SEVEN) return 7;
|
|
65
|
+
if (rank === Rank.EIGHT) return 8;
|
|
66
|
+
if (rank === Rank.NINE) return 9;
|
|
67
|
+
if (rank === Rank.TEN) return 10;
|
|
68
|
+
if (rank === Rank.JACK) return 11;
|
|
69
|
+
if (rank === Rank.QUEEN) return 12;
|
|
70
|
+
if (rank === Rank.KING) return 13;
|
|
71
|
+
if (rank === Rank.ACE) return 14;
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Card class representing a single playing card
|
|
78
|
+
*/
|
|
79
|
+
export class Card {
|
|
80
|
+
suit: string;
|
|
81
|
+
rank: string;
|
|
82
|
+
|
|
83
|
+
constructor(suit: string, rank: string) {
|
|
84
|
+
this.suit = suit;
|
|
85
|
+
this.rank = rank;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static fromString(str: string): Card | null {
|
|
89
|
+
// Format: "As" (Ace of spades), "Kh" (King of hearts), "2d" (2 of diamonds), "Tc" (10 of clubs)
|
|
90
|
+
if (str.length < 2) return null;
|
|
91
|
+
|
|
92
|
+
const rankStr = str.substring(0, str.length - 1);
|
|
93
|
+
const suitStr = str.substring(str.length - 1);
|
|
94
|
+
|
|
95
|
+
// Map suit characters
|
|
96
|
+
let suit: string = "";
|
|
97
|
+
if (suitStr === "s" || suitStr === "S") suit = Suit.SPADES;
|
|
98
|
+
else if (suitStr === "h" || suitStr === "H") suit = Suit.HEARTS;
|
|
99
|
+
else if (suitStr === "d" || suitStr === "D") suit = Suit.DIAMONDS;
|
|
100
|
+
else if (suitStr === "c" || suitStr === "C") suit = Suit.CLUBS;
|
|
101
|
+
else return null;
|
|
102
|
+
|
|
103
|
+
// Map rank characters
|
|
104
|
+
let rank: string = "";
|
|
105
|
+
if (rankStr === "2") rank = Rank.TWO;
|
|
106
|
+
else if (rankStr === "3") rank = Rank.THREE;
|
|
107
|
+
else if (rankStr === "4") rank = Rank.FOUR;
|
|
108
|
+
else if (rankStr === "5") rank = Rank.FIVE;
|
|
109
|
+
else if (rankStr === "6") rank = Rank.SIX;
|
|
110
|
+
else if (rankStr === "7") rank = Rank.SEVEN;
|
|
111
|
+
else if (rankStr === "8") rank = Rank.EIGHT;
|
|
112
|
+
else if (rankStr === "9") rank = Rank.NINE;
|
|
113
|
+
else if (rankStr === "T" || rankStr === "t" || rankStr === "10") rank = Rank.TEN;
|
|
114
|
+
else if (rankStr === "J" || rankStr === "j") rank = Rank.JACK;
|
|
115
|
+
else if (rankStr === "Q" || rankStr === "q") rank = Rank.QUEEN;
|
|
116
|
+
else if (rankStr === "K" || rankStr === "k") rank = Rank.KING;
|
|
117
|
+
else if (rankStr === "A" || rankStr === "a") rank = Rank.ACE;
|
|
118
|
+
else return null;
|
|
119
|
+
|
|
120
|
+
return new Card(suit, rank);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
toString(): string {
|
|
124
|
+
let rankStr: string = "";
|
|
125
|
+
if (this.rank === Rank.TWO) rankStr = "2";
|
|
126
|
+
else if (this.rank === Rank.THREE) rankStr = "3";
|
|
127
|
+
else if (this.rank === Rank.FOUR) rankStr = "4";
|
|
128
|
+
else if (this.rank === Rank.FIVE) rankStr = "5";
|
|
129
|
+
else if (this.rank === Rank.SIX) rankStr = "6";
|
|
130
|
+
else if (this.rank === Rank.SEVEN) rankStr = "7";
|
|
131
|
+
else if (this.rank === Rank.EIGHT) rankStr = "8";
|
|
132
|
+
else if (this.rank === Rank.NINE) rankStr = "9";
|
|
133
|
+
else if (this.rank === Rank.TEN) rankStr = "T";
|
|
134
|
+
else if (this.rank === Rank.JACK) rankStr = "J";
|
|
135
|
+
else if (this.rank === Rank.QUEEN) rankStr = "Q";
|
|
136
|
+
else if (this.rank === Rank.KING) rankStr = "K";
|
|
137
|
+
else if (this.rank === Rank.ACE) rankStr = "A";
|
|
138
|
+
else rankStr = "?";
|
|
139
|
+
|
|
140
|
+
let suitStr: string = "";
|
|
141
|
+
if (this.suit === Suit.SPADES) suitStr = "s";
|
|
142
|
+
else if (this.suit === Suit.HEARTS) suitStr = "h";
|
|
143
|
+
else if (this.suit === Suit.DIAMONDS) suitStr = "d";
|
|
144
|
+
else if (this.suit === Suit.CLUBS) suitStr = "c";
|
|
145
|
+
else suitStr = "?";
|
|
146
|
+
|
|
147
|
+
return rankStr + suitStr;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
equals(other: Card | null): bool {
|
|
151
|
+
if (other === null) return false;
|
|
152
|
+
return this.suit === other.suit && this.rank === other.rank;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getValue(): i32 {
|
|
156
|
+
return Rank.getValue(this.rank);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Hand rank types for poker evaluation
|
|
162
|
+
*/
|
|
163
|
+
export class HandType {
|
|
164
|
+
static readonly HIGH_CARD: i32 = 1;
|
|
165
|
+
static readonly PAIR: i32 = 2;
|
|
166
|
+
static readonly TWO_PAIR: i32 = 3;
|
|
167
|
+
static readonly THREE_OF_A_KIND: i32 = 4;
|
|
168
|
+
static readonly STRAIGHT: i32 = 5;
|
|
169
|
+
static readonly FLUSH: i32 = 6;
|
|
170
|
+
static readonly FULL_HOUSE: i32 = 7;
|
|
171
|
+
static readonly FOUR_OF_A_KIND: i32 = 8;
|
|
172
|
+
static readonly STRAIGHT_FLUSH: i32 = 9;
|
|
173
|
+
static readonly ROYAL_FLUSH: i32 = 10;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* HandRank represents the strength of a poker hand
|
|
178
|
+
*/
|
|
179
|
+
export class HandRank {
|
|
180
|
+
handType: i32;
|
|
181
|
+
kickers: i32[]; // Sorted descending, used for tie-breaking
|
|
182
|
+
|
|
183
|
+
constructor(handType: i32, kickers: i32[] = []) {
|
|
184
|
+
this.handType = handType;
|
|
185
|
+
this.kickers = kickers;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Compare two hand ranks
|
|
190
|
+
* Returns: -1 if this < other, 0 if equal, 1 if this > other
|
|
191
|
+
*/
|
|
192
|
+
compare(other: HandRank): i32 {
|
|
193
|
+
if (this.handType < other.handType) return -1;
|
|
194
|
+
if (this.handType > other.handType) return 1;
|
|
195
|
+
|
|
196
|
+
// Same hand type, compare kickers
|
|
197
|
+
const maxLen = this.kickers.length > other.kickers.length ? this.kickers.length : other.kickers.length;
|
|
198
|
+
for (let i = 0; i < maxLen; i++) {
|
|
199
|
+
const thisKicker = i < this.kickers.length ? this.kickers[i] : 0;
|
|
200
|
+
const otherKicker = i < other.kickers.length ? other.kickers[i] : 0;
|
|
201
|
+
if (thisKicker < otherKicker) return -1;
|
|
202
|
+
if (thisKicker > otherKicker) return 1;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Deck class representing a standard 52-card deck
|
|
211
|
+
*/
|
|
212
|
+
export class Deck {
|
|
213
|
+
cards: Card[];
|
|
214
|
+
|
|
215
|
+
constructor() {
|
|
216
|
+
this.cards = new Array<Card>(52);
|
|
217
|
+
let idx = 0;
|
|
218
|
+
for (let s = 0; s < Suit.ALL.length; s++) {
|
|
219
|
+
for (let r = 0; r < Rank.ALL.length; r++) {
|
|
220
|
+
this.cards[idx] = new Card(Suit.ALL[s], Rank.ALL[r]);
|
|
221
|
+
idx++;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Create a new deck from existing cards (for deserialization)
|
|
228
|
+
*/
|
|
229
|
+
static fromCards(cards: Card[]): Deck {
|
|
230
|
+
const deck = new Deck();
|
|
231
|
+
deck.cards = cards;
|
|
232
|
+
return deck;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Shuffle the deck using Fisher-Yates algorithm with deterministic randomness
|
|
237
|
+
*/
|
|
238
|
+
shuffle(seed: RandomSeed): RandomSeed {
|
|
239
|
+
const n = this.cards.length;
|
|
240
|
+
let currentSeed = seed;
|
|
241
|
+
|
|
242
|
+
for (let i = n - 1; i > 0; i--) {
|
|
243
|
+
// Get random index from 0 to i (inclusive)
|
|
244
|
+
const result = getRandomIntInRange(currentSeed, 0, i + 1);
|
|
245
|
+
const j = <i32>result.value;
|
|
246
|
+
currentSeed = result.seed;
|
|
247
|
+
|
|
248
|
+
// Swap cards[i] and cards[j]
|
|
249
|
+
const temp = this.cards[i];
|
|
250
|
+
this.cards[i] = this.cards[j];
|
|
251
|
+
this.cards[j] = temp;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return currentSeed;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Deal one card from the top of the deck
|
|
259
|
+
*/
|
|
260
|
+
dealCard(): Card | null {
|
|
261
|
+
if (this.cards.length === 0) return null;
|
|
262
|
+
return this.cards.pop();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Deal multiple cards
|
|
267
|
+
*/
|
|
268
|
+
dealCards(count: i32): Card[] {
|
|
269
|
+
const dealt = new Array<Card>(0);
|
|
270
|
+
for (let i = 0; i < count; i++) {
|
|
271
|
+
const card = this.dealCard();
|
|
272
|
+
if (card !== null) {
|
|
273
|
+
dealt.push(card);
|
|
274
|
+
} else {
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return dealt;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Burn a card (remove without returning)
|
|
283
|
+
*/
|
|
284
|
+
burnCard(): bool {
|
|
285
|
+
return this.dealCard() !== null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Get number of cards remaining
|
|
290
|
+
*/
|
|
291
|
+
size(): i32 {
|
|
292
|
+
return this.cards.length;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Check if deck is empty
|
|
297
|
+
*/
|
|
298
|
+
isEmpty(): bool {
|
|
299
|
+
return this.cards.length === 0;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get a copy of the deck
|
|
304
|
+
*/
|
|
305
|
+
clone(): Deck {
|
|
306
|
+
const newDeck = new Deck();
|
|
307
|
+
newDeck.cards = new Array<Card>(this.cards.length);
|
|
308
|
+
for (let i = 0; i < this.cards.length; i++) {
|
|
309
|
+
newDeck.cards[i] = this.cards[i];
|
|
310
|
+
}
|
|
311
|
+
return newDeck;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|