@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
package/README.md
ADDED
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
# @arcanahq/cardgames
|
|
2
|
+
|
|
3
|
+
Card game utilities and blackjack action processing library for Arcana contracts.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This library provides reusable card game logic that can be shared across different card game implementations. It builds on top of `@arcanahq/core` and provides:
|
|
8
|
+
|
|
9
|
+
- **Deck Management**: Deterministic shuffling and dealing for both standard (52-card) and Spanish 21 (48-card) decks
|
|
10
|
+
- **Card Index Mapping**: Efficient index-based card representation (0-51 for standard, 0-47 for Spanish 21)
|
|
11
|
+
- **Blackjack Rules Configuration**: Configurable rules for different blackjack variants (standard, Spanish 21, etc.)
|
|
12
|
+
- **Blackjack Action Processing**: Utility functions for validating and processing blackjack actions
|
|
13
|
+
- **Poker Game Utilities**: Hand evaluation, pot management, blinds/antes posting, betting rounds, rake calculation
|
|
14
|
+
- **Cash Game Utilities**: Buy-in validation, rake calculation, rebuy management
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @arcanahq/cardgames
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or use as a local dependency:
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@arcanahq/cardgames": "file:../packages/cardgames"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Structure
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
@cardgames/
|
|
35
|
+
├── assembly/
|
|
36
|
+
│ ├── blackjack/
|
|
37
|
+
│ │ ├── rules.ts # Blackjack rules configuration
|
|
38
|
+
│ │ └── actions.ts # Blackjack action processing utilities
|
|
39
|
+
│ ├── deck/
|
|
40
|
+
│ │ ├── deck.ts # Deck management (shuffling, dealing, index mapping)
|
|
41
|
+
│ │ └── index.ts # Deck module exports
|
|
42
|
+
│ ├── cards.ts # Card, Suit, Rank classes
|
|
43
|
+
│ ├── cardgames.ts # Card game utilities (deck creation, shuffling, dealing)
|
|
44
|
+
│ └── index.ts # Main entry point
|
|
45
|
+
└── package.json
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
### Deck Management
|
|
51
|
+
|
|
52
|
+
#### Single Deck (52 cards)
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { DeckConfig, dealCardByIndex, createShuffledDeck } from "@arcanahq/cardgames/assembly/deck";
|
|
56
|
+
|
|
57
|
+
const config = DeckConfig.standard();
|
|
58
|
+
|
|
59
|
+
// Deal a card deterministically
|
|
60
|
+
const card = dealCardByIndex("shuffle-id", "shuffle-salt", 0, config);
|
|
61
|
+
|
|
62
|
+
// Create full shuffled deck
|
|
63
|
+
const deck = createShuffledDeck("shuffle-id", "shuffle-salt", 0, config);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
#### Spanish 21 Deck (48 cards, no 10s)
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
const config = DeckConfig.spanish21();
|
|
70
|
+
|
|
71
|
+
// Deal a card from Spanish 21 deck
|
|
72
|
+
const card = dealCardByIndex("shuffle-id", "shuffle-salt", 0, config);
|
|
73
|
+
// card will never be a 10
|
|
74
|
+
|
|
75
|
+
// Create full shuffled Spanish 21 deck
|
|
76
|
+
const deck = createShuffledDeck("shuffle-id", "shuffle-salt", 0, config);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
#### Multi-Deck Shoes
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { ShoeConfig, dealCardFromShoe, DeckConfig } from "@arcanahq/cardgames/assembly/deck";
|
|
83
|
+
|
|
84
|
+
// Standard shoe with any number of decks
|
|
85
|
+
const shoe6 = ShoeConfig.standard(6); // 6-deck shoe (312 cards)
|
|
86
|
+
const shoe8 = ShoeConfig.standard(8); // 8-deck shoe (416 cards)
|
|
87
|
+
const shoeCustom = ShoeConfig.standard(10); // 10-deck shoe (520 cards)
|
|
88
|
+
|
|
89
|
+
// Spanish 21 shoe with any number of decks
|
|
90
|
+
const spanishShoe6 = ShoeConfig.spanish21(6); // 6-deck Spanish 21 (288 cards)
|
|
91
|
+
const spanishShoe8 = ShoeConfig.spanish21(8); // 8-deck Spanish 21 (384 cards)
|
|
92
|
+
|
|
93
|
+
// Create shoe with any custom deck configuration
|
|
94
|
+
const spanishDeck = DeckConfig.spanish21();
|
|
95
|
+
const customShoe = ShoeConfig.withDeck(spanishDeck, 6); // 6-deck Spanish 21
|
|
96
|
+
|
|
97
|
+
// Or with a custom deck config
|
|
98
|
+
// Use standard or Spanish 21, or create your own DeckConfig subclass
|
|
99
|
+
const customDeck = DeckConfig.standard(); // or DeckConfig.spanish21()
|
|
100
|
+
const shoe = ShoeConfig.withDeck(customDeck, 4); // 4-deck shoe
|
|
101
|
+
|
|
102
|
+
// Deal cards from shoe (tracks position, auto-reshuffles when exhausted)
|
|
103
|
+
let shoePosition = 0;
|
|
104
|
+
const card1 = dealCardFromShoe("shuffle-id", "shuffle-salt", shoePosition++, shoe6);
|
|
105
|
+
const card2 = dealCardFromShoe("shuffle-id", "shuffle-salt", shoePosition++, shoe6);
|
|
106
|
+
// Position persists across game sessions
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
#### Card Index Mapping
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { CardIndexMapper } from "@arcanahq/cardgames/assembly/deck";
|
|
113
|
+
|
|
114
|
+
const config = DeckConfig.standard();
|
|
115
|
+
|
|
116
|
+
// Convert index to card
|
|
117
|
+
const card = CardIndexMapper.indexToCard(0, config); // 2 of Spades
|
|
118
|
+
const card2 = CardIndexMapper.indexToCard(51, config); // Ace of Clubs
|
|
119
|
+
|
|
120
|
+
// Convert card to index
|
|
121
|
+
const index = CardIndexMapper.cardToIndex(card, config); // 0
|
|
122
|
+
|
|
123
|
+
// Create unshuffled deck indices
|
|
124
|
+
const indices = CardIndexMapper.createUnshuffledDeckIndices(config);
|
|
125
|
+
// [0, 1, 2, ..., 51]
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### Deterministic Shuffling
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { deterministicShuffleIndices, CardIndexMapper } from "@arcanahq/cardgames/assembly/deck";
|
|
132
|
+
|
|
133
|
+
const config = DeckConfig.standard();
|
|
134
|
+
const unshuffledIndices = CardIndexMapper.createUnshuffledDeckIndices(config);
|
|
135
|
+
|
|
136
|
+
// Shuffle deterministically
|
|
137
|
+
const shuffledIndices = deterministicShuffleIndices(
|
|
138
|
+
unshuffledIndices,
|
|
139
|
+
"shuffle-id",
|
|
140
|
+
"shuffle-salt",
|
|
141
|
+
0 // seedIndex
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Same parameters = same shuffle
|
|
145
|
+
const shuffled2 = deterministicShuffleIndices(
|
|
146
|
+
unshuffledIndices,
|
|
147
|
+
"shuffle-id",
|
|
148
|
+
"shuffle-salt",
|
|
149
|
+
0
|
|
150
|
+
);
|
|
151
|
+
// shuffledIndices === shuffled2
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Blackjack Rules
|
|
155
|
+
|
|
156
|
+
#### Standard Rules
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import { BlackjackRules } from "@arcanahq/cardgames/assembly/blackjack/rules";
|
|
160
|
+
|
|
161
|
+
// Use standard rules (most common casino rules)
|
|
162
|
+
const rules = BlackjackRules.standard();
|
|
163
|
+
// - Dealer stands on 17
|
|
164
|
+
// - No hit on soft 17
|
|
165
|
+
// - No double after split
|
|
166
|
+
// - Surrender allowed
|
|
167
|
+
// - Insurance offered
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
#### Spanish 21 Rules
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
const rules = BlackjackRules.spanish21();
|
|
174
|
+
// - 48-card deck (no 10s)
|
|
175
|
+
// - Dealer hits on soft 17
|
|
176
|
+
// - Double after split allowed
|
|
177
|
+
// - No insurance
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
#### Custom Rules
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
const rules = new BlackjackRules(
|
|
184
|
+
17, // dealerStandValue
|
|
185
|
+
false, // hitOnSoft17
|
|
186
|
+
4, // maxSplitHands
|
|
187
|
+
false, // doubleAfterSplit
|
|
188
|
+
true, // surrenderAllowed
|
|
189
|
+
false, // lateSurrender
|
|
190
|
+
true, // insuranceOffered
|
|
191
|
+
false // isSpanish21
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// Or use factory methods
|
|
195
|
+
const rules = BlackjackRules.dealerHitsSoft17();
|
|
196
|
+
const rules = BlackjackRules.allowDoubleAfterSplit();
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
#### Custom Payouts
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
const rules = new BlackjackRules(
|
|
203
|
+
17, false, 4, false, true, false, true, false,
|
|
204
|
+
2.0, // payoutBlackjack (6:5 instead of 3:2)
|
|
205
|
+
1.0, // payoutWin
|
|
206
|
+
1.0, // payoutPush
|
|
207
|
+
0.0, // payoutLose
|
|
208
|
+
0.5, // payoutSurrender
|
|
209
|
+
2.0 // payoutInsurance
|
|
210
|
+
);
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Blackjack Action Validation
|
|
214
|
+
|
|
215
|
+
#### Calculate Available Actions
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
import { calculateAvailableActions } from "@arcanahq/cardgames/assembly/blackjack/actions";
|
|
219
|
+
|
|
220
|
+
const actions = calculateAvailableActions(
|
|
221
|
+
hand.cards.length, // handCardsLength
|
|
222
|
+
hand.isFromSplit, // handIsFromSplit
|
|
223
|
+
hand.isSplitAces, // handIsSplitAces
|
|
224
|
+
hand.isStanding, // handIsStanding
|
|
225
|
+
hand.isBusted, // handIsBusted
|
|
226
|
+
state.gamePhase, // gamePhase
|
|
227
|
+
state.playerHands.length, // playerHandsCount
|
|
228
|
+
canSplitCards(hand.cards), // canSplit (pre-calculated)
|
|
229
|
+
rules
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
// Use actions
|
|
233
|
+
if (actions.canStand) { /* ... */ }
|
|
234
|
+
if (actions.canDouble) { /* ... */ }
|
|
235
|
+
if (actions.canSplit) { /* ... */ }
|
|
236
|
+
if (actions.canSurrender) { /* ... */ }
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
#### Validate Actions
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
import {
|
|
243
|
+
validateCanDouble,
|
|
244
|
+
validateCanSplit,
|
|
245
|
+
validateCanSurrender,
|
|
246
|
+
validateCanHit,
|
|
247
|
+
validateCanStand,
|
|
248
|
+
validateActionPhase,
|
|
249
|
+
validateActiveHand
|
|
250
|
+
} from "@arcanahq/cardgames/assembly/blackjack/actions";
|
|
251
|
+
|
|
252
|
+
// Validate double action
|
|
253
|
+
validateCanDouble(
|
|
254
|
+
hand.cards.length,
|
|
255
|
+
hand.isSplitAces,
|
|
256
|
+
hand.isFromSplit,
|
|
257
|
+
rules
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// Validate split action
|
|
261
|
+
validateCanSplit(
|
|
262
|
+
hand.cards.length,
|
|
263
|
+
state.playerHands.length,
|
|
264
|
+
canSplitCards(hand.cards),
|
|
265
|
+
rules
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Validate surrender action
|
|
269
|
+
validateCanSurrender(
|
|
270
|
+
hand.cards.length,
|
|
271
|
+
hand.isFromSplit,
|
|
272
|
+
rules
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// Validate hit action
|
|
276
|
+
validateCanHit(
|
|
277
|
+
hand.isStanding,
|
|
278
|
+
hand.isBusted,
|
|
279
|
+
hand.isSplitAces
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
// Validate stand action
|
|
283
|
+
validateCanStand(
|
|
284
|
+
hand.isStanding,
|
|
285
|
+
hand.isBusted
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// Validate game phase
|
|
289
|
+
validateActionPhase(state.gamePhase, "HIT", "PLAYING");
|
|
290
|
+
|
|
291
|
+
// Validate active hand
|
|
292
|
+
validateActiveHand(currentHandIndex, state.playerHands.length);
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Dealer Logic
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
import { shouldDealerHit, shouldOfferInsurance } from "@arcanahq/cardgames/assembly/blackjack/actions";
|
|
299
|
+
|
|
300
|
+
// Check if dealer should hit
|
|
301
|
+
const dealerShouldHit = shouldDealerHit(
|
|
302
|
+
dealerHandValue,
|
|
303
|
+
rules.dealerStandValue,
|
|
304
|
+
rules.hitOnSoft17,
|
|
305
|
+
isSoftHand(dealerCards)
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
// Check if insurance should be offered
|
|
309
|
+
const offerInsurance = shouldOfferInsurance(
|
|
310
|
+
dealerUpCard.rank,
|
|
311
|
+
rules
|
|
312
|
+
);
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Card Types
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
import { Card, Suit, Rank } from "@arcanahq/cardgames/assembly/cards";
|
|
319
|
+
|
|
320
|
+
// Create a card
|
|
321
|
+
const card = new Card(Suit.HEARTS, Rank.ACE);
|
|
322
|
+
|
|
323
|
+
// Use suit and rank constants
|
|
324
|
+
const suits = Suit.ALL; // [♠, ♥, ♦, ♣]
|
|
325
|
+
const ranks = Rank.ALL; // [2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, K, A]
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Card Game Utilities
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
import {
|
|
332
|
+
createStandardDeck,
|
|
333
|
+
shuffleDeck,
|
|
334
|
+
dealCard,
|
|
335
|
+
dealCards,
|
|
336
|
+
cardToInt,
|
|
337
|
+
intToCard
|
|
338
|
+
} from "@arcanahq/cardgames/assembly/cardgames";
|
|
339
|
+
|
|
340
|
+
// Create a standard 52-card deck
|
|
341
|
+
const deck = createStandardDeck();
|
|
342
|
+
|
|
343
|
+
// Shuffle with deterministic randomness
|
|
344
|
+
const shuffled = shuffleDeck(deck, seed);
|
|
345
|
+
|
|
346
|
+
// Deal cards
|
|
347
|
+
const card = dealCard(deck);
|
|
348
|
+
const cards = dealCards(deck, 5);
|
|
349
|
+
|
|
350
|
+
// Convert between Card and integer (0-51)
|
|
351
|
+
const index = cardToInt(card);
|
|
352
|
+
const card2 = intToCard(index);
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Core Blackjack Functions
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
import {
|
|
359
|
+
dealerShouldHit,
|
|
360
|
+
isSoftHand,
|
|
361
|
+
calculateBlackjackHandValue,
|
|
362
|
+
isBlackjack,
|
|
363
|
+
isBusted,
|
|
364
|
+
canSplitCards
|
|
365
|
+
} from "@arcanahq/cardgames/assembly/blackjack/rules";
|
|
366
|
+
|
|
367
|
+
// Calculate hand value
|
|
368
|
+
const value = calculateBlackjackHandValue(cards);
|
|
369
|
+
|
|
370
|
+
// Check for blackjack
|
|
371
|
+
const hasBlackjack = isBlackjack(cards);
|
|
372
|
+
|
|
373
|
+
// Check for bust
|
|
374
|
+
const isBusted = isBusted(cards);
|
|
375
|
+
|
|
376
|
+
// Check if cards can be split
|
|
377
|
+
const canSplit = canSplitCards(cards);
|
|
378
|
+
|
|
379
|
+
// Check if hand is soft
|
|
380
|
+
const isSoft = isSoftHand(cards);
|
|
381
|
+
|
|
382
|
+
// Check if dealer should hit
|
|
383
|
+
const shouldHit = dealerShouldHit(cards, rules);
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## Extensibility
|
|
387
|
+
|
|
388
|
+
### Creating Custom Rule Variants
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
// Example: European Blackjack (no hole card)
|
|
392
|
+
class EuropeanBlackjackRules extends BlackjackRules {
|
|
393
|
+
static create(): BlackjackRules {
|
|
394
|
+
const rules = BlackjackRules.standard();
|
|
395
|
+
// Customize for European rules
|
|
396
|
+
return rules;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Adding Custom Actions
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
// Extend AvailableActions for custom game variants
|
|
405
|
+
class CustomAvailableActions extends AvailableActions {
|
|
406
|
+
canInsurance: bool = false;
|
|
407
|
+
canEvenMoney: bool = false;
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
## Testing
|
|
412
|
+
|
|
413
|
+
The library includes comprehensive test coverage:
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
npm test
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
All core functions are tested with:
|
|
420
|
+
- Standard blackjack rules
|
|
421
|
+
- Spanish 21 rules
|
|
422
|
+
- Custom rule configurations
|
|
423
|
+
- Edge cases and error conditions
|
|
424
|
+
|
|
425
|
+
## API Reference
|
|
426
|
+
|
|
427
|
+
### BlackjackRules
|
|
428
|
+
|
|
429
|
+
Configurable blackjack rules class with factory methods:
|
|
430
|
+
- `BlackjackRules.standard()` - Standard casino rules
|
|
431
|
+
- `BlackjackRules.spanish21()` - Spanish 21 variant
|
|
432
|
+
- `BlackjackRules.dealerHitsSoft17()` - Dealer hits on soft 17
|
|
433
|
+
- `BlackjackRules.allowDoubleAfterSplit()` - Allow double after split
|
|
434
|
+
|
|
435
|
+
### AvailableActions
|
|
436
|
+
|
|
437
|
+
Result class from `calculateAvailableActions()`:
|
|
438
|
+
- `canStand: bool` - Can player stand
|
|
439
|
+
- `canDouble: bool` - Can player double
|
|
440
|
+
- `canSplit: bool` - Can player split
|
|
441
|
+
- `canSurrender: bool` - Can player surrender
|
|
442
|
+
|
|
443
|
+
### Validation Functions
|
|
444
|
+
|
|
445
|
+
All validation functions throw errors on invalid conditions:
|
|
446
|
+
- `validateActionPhase()` - Validates game phase
|
|
447
|
+
- `validateActiveHand()` - Validates hand exists
|
|
448
|
+
- `validateCanHit()` - Validates hit action
|
|
449
|
+
- `validateCanStand()` - Validates stand action
|
|
450
|
+
- `validateCanDouble()` - Validates double action
|
|
451
|
+
- `validateCanSplit()` - Validates split action
|
|
452
|
+
- `validateCanSurrender()` - Validates surrender action
|
|
453
|
+
|
|
454
|
+
### Utility Functions
|
|
455
|
+
|
|
456
|
+
- `calculateAvailableActions()` - Calculate all available actions
|
|
457
|
+
- `shouldDealerHit()` - Determine if dealer should hit
|
|
458
|
+
- `shouldOfferInsurance()` - Determine if insurance should be offered
|
|
459
|
+
|
|
460
|
+
## Dependencies
|
|
461
|
+
|
|
462
|
+
- `@arcanahq/core`: Core framework for Arcana contracts
|
|
463
|
+
|
|
464
|
+
## Poker Game Utilities
|
|
465
|
+
|
|
466
|
+
The library includes comprehensive utilities for poker-style card games:
|
|
467
|
+
|
|
468
|
+
### Stakes and Betting
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
import { Stakes, AnteType, BettingRoundState, PokerSeatBase } from "@arcanahq/cardgames/assembly/poker/poker_game_types";
|
|
472
|
+
import {
|
|
473
|
+
calculateAnteAmount,
|
|
474
|
+
postAntes,
|
|
475
|
+
postBlinds,
|
|
476
|
+
getNextActingSeat,
|
|
477
|
+
validateBuyIn,
|
|
478
|
+
processBuyIn,
|
|
479
|
+
isBettingRoundComplete
|
|
480
|
+
} from "@arcanahq/cardgames/assembly/poker/poker_game_utils";
|
|
481
|
+
|
|
482
|
+
// Create stakes configuration
|
|
483
|
+
const stakes = new Stakes(10, 20, 5); // SB: 10, BB: 20, Ante: 5
|
|
484
|
+
|
|
485
|
+
// Calculate ante amount
|
|
486
|
+
const anteAmount = calculateAnteAmount(stakes, AnteType.FIXED, 0.0);
|
|
487
|
+
|
|
488
|
+
// Post antes for all players
|
|
489
|
+
const bettingState = new BettingRoundState();
|
|
490
|
+
const antesResult = postAntes(seats, stakes, AnteType.FIXED, 0.0, bettingState);
|
|
491
|
+
// antesResult.seats - updated seats with antes deducted
|
|
492
|
+
// antesResult.totalAnteCollected - total ante collected
|
|
493
|
+
|
|
494
|
+
// Post small blind and big blind
|
|
495
|
+
const blindsResult = postBlinds(seats, stakes, sbSeatId, bbSeatId, bettingState);
|
|
496
|
+
// blindsResult.seats - updated seats with blinds deducted
|
|
497
|
+
// blindsResult.currentBetToMatch - current bet to match (BB amount)
|
|
498
|
+
|
|
499
|
+
// Get next player to act
|
|
500
|
+
const nextSeatId = getNextActingSeat(seats, buttonSeatId, true, bettingState); // true = preflop
|
|
501
|
+
|
|
502
|
+
// Validate and process buy-in
|
|
503
|
+
if (validateBuyIn(buyInAmount, minBuyIn, maxBuyIn)) {
|
|
504
|
+
const updatedSeat = processBuyIn(seat, buyInAmount, minBuyIn, maxBuyIn);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Check if betting round is complete
|
|
508
|
+
const isComplete = isBettingRoundComplete(seats, bettingState);
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### Showdown Utilities
|
|
512
|
+
|
|
513
|
+
The showdown system uses an interface pattern, allowing different implementations for various poker variants.
|
|
514
|
+
|
|
515
|
+
#### Standard Hold'em (Default)
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
import {
|
|
519
|
+
compareHandsShowdown,
|
|
520
|
+
compareFiveCardHands,
|
|
521
|
+
getPlayerHandRank,
|
|
522
|
+
getPlayerBestHand,
|
|
523
|
+
compareTwoHands,
|
|
524
|
+
StandardShowdownEvaluator
|
|
525
|
+
} from "@arcanahq/cardgames/assembly/poker/showdown";
|
|
526
|
+
import { Card } from "@arcanahq/cardgames/assembly/cards";
|
|
527
|
+
|
|
528
|
+
// Compare multiple players' hands in a showdown
|
|
529
|
+
const holeCardsMap = new Map<i32, Card[]>();
|
|
530
|
+
holeCardsMap.set(0, player0HoleCards);
|
|
531
|
+
holeCardsMap.set(1, player1HoleCards);
|
|
532
|
+
holeCardsMap.set(2, player2HoleCards);
|
|
533
|
+
|
|
534
|
+
const communityCards = [flop, turn, river]; // 5 cards total
|
|
535
|
+
|
|
536
|
+
const result = compareHandsShowdown(holeCardsMap, communityCards);
|
|
537
|
+
// result.winners - array of seat IDs that tied for the win
|
|
538
|
+
// result.handRanks - map of seat ID to their HandRank
|
|
539
|
+
// result.bestFiveCards - map of seat ID to their best 5-card hand
|
|
540
|
+
|
|
541
|
+
// Compare exactly 5-card hands (for games like 5-card draw)
|
|
542
|
+
const fiveCardHands = new Map<i32, Card[]>();
|
|
543
|
+
fiveCardHands.set(0, player0FiveCards);
|
|
544
|
+
fiveCardHands.set(1, player1FiveCards);
|
|
545
|
+
const result = compareFiveCardHands(fiveCardHands);
|
|
546
|
+
|
|
547
|
+
// Get a single player's hand rank
|
|
548
|
+
const handRank = getPlayerHandRank(holeCards, communityCards);
|
|
549
|
+
|
|
550
|
+
// Get a single player's best 5-card hand
|
|
551
|
+
const bestHand = getPlayerBestHand(holeCards, communityCards);
|
|
552
|
+
|
|
553
|
+
// Compare two specific hands
|
|
554
|
+
const winner = compareTwoHands(holeCards1, holeCards2, communityCards);
|
|
555
|
+
// Returns: 1 if hand1 wins, -1 if hand2 wins, 0 if tie
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
#### Six-Plus Hold'em
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
import { SixPlusShowdownEvaluator } from "@arcanahq/cardgames/assembly/poker/six_plus_showdown";
|
|
562
|
+
import { Card } from "@arcanahq/cardgames/assembly/cards";
|
|
563
|
+
|
|
564
|
+
// Create Six-Plus evaluator (uses 36-card deck, different hand rankings)
|
|
565
|
+
const evaluator = new SixPlusShowdownEvaluator();
|
|
566
|
+
|
|
567
|
+
// Use the evaluator for showdown
|
|
568
|
+
const result = evaluator.compareHandsShowdown(holeCardsMap, communityCards);
|
|
569
|
+
// result.winners - array of seat IDs that tied for the win
|
|
570
|
+
|
|
571
|
+
// Six-Plus hand rankings:
|
|
572
|
+
// - Flush beats Full House (opposite of standard)
|
|
573
|
+
// - Three of a Kind beats Straight (opposite of standard)
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
#### Stud Poker (5-Card and 7-Card)
|
|
577
|
+
|
|
578
|
+
```typescript
|
|
579
|
+
import { StudShowdownEvaluator } from "@arcanahq/cardgames/assembly/poker/stud_evaluator";
|
|
580
|
+
import { Card } from "@arcanahq/cardgames/assembly/cards";
|
|
581
|
+
|
|
582
|
+
const evaluator = new StudShowdownEvaluator();
|
|
583
|
+
|
|
584
|
+
// For 7-card stud: each player has 7 cards, choose best 5
|
|
585
|
+
const sevenCardHands = new Map<i32, Card[]>();
|
|
586
|
+
sevenCardHands.set(0, player0SevenCards); // 7 cards
|
|
587
|
+
sevenCardHands.set(1, player1SevenCards); // 7 cards
|
|
588
|
+
|
|
589
|
+
// No community cards for stud
|
|
590
|
+
const result = evaluator.compareHandsShowdown(sevenCardHands, new Array<Card>(0));
|
|
591
|
+
|
|
592
|
+
// For 5-card stud: each player has 5 cards, use all 5
|
|
593
|
+
const fiveCardHands = new Map<i32, Card[]>();
|
|
594
|
+
fiveCardHands.set(0, player0FiveCards); // 5 cards
|
|
595
|
+
fiveCardHands.set(1, player1FiveCards); // 5 cards
|
|
596
|
+
|
|
597
|
+
const result = evaluator.compareHandsShowdown(fiveCardHands, new Array<Card>(0));
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
#### Omaha
|
|
601
|
+
|
|
602
|
+
```typescript
|
|
603
|
+
import { OmahaShowdownEvaluator } from "@arcanahq/cardgames/assembly/poker/omaha_evaluator";
|
|
604
|
+
import { Card } from "@arcanahq/cardgames/assembly/cards";
|
|
605
|
+
|
|
606
|
+
const evaluator = new OmahaShowdownEvaluator();
|
|
607
|
+
|
|
608
|
+
// Omaha: 4 hole cards, must use exactly 2 + 3 community cards
|
|
609
|
+
const holeCardsMap = new Map<i32, Card[]>();
|
|
610
|
+
holeCardsMap.set(0, player0FourCards); // 4 cards
|
|
611
|
+
holeCardsMap.set(1, player1FourCards); // 4 cards
|
|
612
|
+
|
|
613
|
+
const communityCards = [flop, turn, river]; // 5 cards
|
|
614
|
+
|
|
615
|
+
const result = evaluator.compareHandsShowdown(holeCardsMap, communityCards);
|
|
616
|
+
// Automatically tries all combinations of 2 hole + 3 community cards
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
#### Using Variant Configurations
|
|
620
|
+
|
|
621
|
+
```typescript
|
|
622
|
+
import {
|
|
623
|
+
VariantConfigs,
|
|
624
|
+
PokerVariant,
|
|
625
|
+
getEvaluatorForVariant,
|
|
626
|
+
createEvaluator
|
|
627
|
+
} from "@arcanahq/cardgames/assembly/poker/variants";
|
|
628
|
+
import { ShowdownEvaluator } from "@arcanahq/cardgames/assembly/poker/showdown_evaluator";
|
|
629
|
+
|
|
630
|
+
// Get evaluator by variant name
|
|
631
|
+
const holdemEvaluator = getEvaluatorForVariant(PokerVariant.TEXAS_HOLDEM);
|
|
632
|
+
const studEvaluator = getEvaluatorForVariant(PokerVariant.SEVEN_CARD_STUD);
|
|
633
|
+
const omahaEvaluator = getEvaluatorForVariant(PokerVariant.OMAHA);
|
|
634
|
+
const sixPlusEvaluator = getEvaluatorForVariant(PokerVariant.SIX_PLUS_HOLDEM);
|
|
635
|
+
|
|
636
|
+
// Or use variant configuration
|
|
637
|
+
const config = VariantConfigs.sevenCardStud();
|
|
638
|
+
const evaluator = createEvaluator(config);
|
|
639
|
+
|
|
640
|
+
// Available configurations:
|
|
641
|
+
// - VariantConfigs.texasHoldem()
|
|
642
|
+
// - VariantConfigs.omaha()
|
|
643
|
+
// - VariantConfigs.omahaHiLo()
|
|
644
|
+
// - VariantConfigs.sevenCardStud()
|
|
645
|
+
// - VariantConfigs.fiveCardStud()
|
|
646
|
+
// - VariantConfigs.razz() // Lowball 7-card stud
|
|
647
|
+
// - VariantConfigs.sixPlusHoldem()
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
#### Custom Evaluator
|
|
651
|
+
|
|
652
|
+
```typescript
|
|
653
|
+
import { ShowdownEvaluator, ShowdownResult } from "@arcanahq/cardgames/assembly/poker/showdown_evaluator";
|
|
654
|
+
import { Card, HandRank } from "@arcanahq/cardgames/assembly/cards";
|
|
655
|
+
|
|
656
|
+
// Extend ShowdownEvaluator for custom poker variants
|
|
657
|
+
class CustomShowdownEvaluator extends ShowdownEvaluator {
|
|
658
|
+
evaluateHand(holeCards: Card[], communityCards: Card[]): HandRank {
|
|
659
|
+
// Custom evaluation logic
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
getBestFiveCards(holeCards: Card[], communityCards: Card[]): Card[] {
|
|
663
|
+
// Custom best hand selection
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
compareHands(hand1: HandRank, hand2: HandRank): i32 {
|
|
667
|
+
// Custom comparison logic
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const customEvaluator = new CustomShowdownEvaluator();
|
|
672
|
+
const result = customEvaluator.compareHandsShowdown(holeCardsMap, communityCards);
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
### Pot Management
|
|
676
|
+
|
|
677
|
+
```typescript
|
|
678
|
+
import {
|
|
679
|
+
constructSidePots,
|
|
680
|
+
calculatePokerRake,
|
|
681
|
+
distributePot,
|
|
682
|
+
splitPotForRuns,
|
|
683
|
+
lockPots
|
|
684
|
+
} from "@arcanahq/cardgames/assembly/poker/poker_game_utils";
|
|
685
|
+
|
|
686
|
+
// Construct side pots from player contributions
|
|
687
|
+
const contributions = new Map<i32, i64>();
|
|
688
|
+
contributions.set(seat1Id, 100);
|
|
689
|
+
contributions.set(seat2Id, 200); // All-in
|
|
690
|
+
const pots = constructSidePots(contributions, 0);
|
|
691
|
+
|
|
692
|
+
// Calculate rake
|
|
693
|
+
const rakeConfig = new PokerRakeConfig(5.0, 10); // 5% rake, cap at 10
|
|
694
|
+
const rake = calculatePokerRake(potAmount, rakeConfig);
|
|
695
|
+
|
|
696
|
+
// Distribute pot to winners
|
|
697
|
+
const distribution = distributePot(pot, winners, runIndex, buttonSeatId, rakeConfig);
|
|
698
|
+
// distribution.payouts - Map of seat ID to payout amount
|
|
699
|
+
// distribution.rake - Rake amount deducted
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
### Betting Round State
|
|
703
|
+
|
|
704
|
+
```typescript
|
|
705
|
+
import { BettingRoundState } from "@arcanahq/cardgames/assembly/poker/poker_game_types";
|
|
706
|
+
|
|
707
|
+
const bettingState = new BettingRoundState();
|
|
708
|
+
|
|
709
|
+
// Track contributions
|
|
710
|
+
bettingState.contribThisRound.set(seatId, amount);
|
|
711
|
+
bettingState.contribTotal.set(seatId, totalAmount);
|
|
712
|
+
|
|
713
|
+
// Calculate amount to call
|
|
714
|
+
const toCall = bettingState.calculateToCall(seatId);
|
|
715
|
+
|
|
716
|
+
// Reset for next street
|
|
717
|
+
bettingState.resetRound();
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
## License
|
|
721
|
+
|
|
722
|
+
MIT
|