@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,142 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Shared types for cash games
|
|
4
|
+
*
|
|
5
|
+
* Standardized interfaces for buy-in, rake, rebuy, and seat management
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Rake configuration for cash games
|
|
10
|
+
*/
|
|
11
|
+
export class RakeConfig {
|
|
12
|
+
percentage: f64 = 0.0; // Rake percentage (e.g., 5.0 for 5%)
|
|
13
|
+
cap: i64 = 0; // Maximum rake per pot (0 = no cap)
|
|
14
|
+
|
|
15
|
+
constructor(percentage: f64 = 0.0, cap: i64 = 0) {
|
|
16
|
+
this.percentage = percentage;
|
|
17
|
+
this.cap = cap;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
clone(): RakeConfig {
|
|
21
|
+
return new RakeConfig(this.percentage, this.cap);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Buy-in range configuration
|
|
27
|
+
*/
|
|
28
|
+
export class BuyInRange {
|
|
29
|
+
min: string = "0"; // Minimum buy-in amount (BigInt string)
|
|
30
|
+
max: string = "0"; // Maximum buy-in amount (BigInt string)
|
|
31
|
+
|
|
32
|
+
constructor(min: string = "0", max: string = "0") {
|
|
33
|
+
this.min = min;
|
|
34
|
+
this.max = max;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
clone(): BuyInRange {
|
|
38
|
+
return new BuyInRange(this.min, this.max);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Rebuy configuration
|
|
44
|
+
*/
|
|
45
|
+
export class RebuyConfig {
|
|
46
|
+
allowed: bool = true; // Whether rebuys are allowed
|
|
47
|
+
cooldownMs: i64 = 0; // Cooldown between rebuys (0 = no cooldown)
|
|
48
|
+
maxRebuys: i32 = -1; // Maximum rebuys per player (-1 = unlimited)
|
|
49
|
+
autoRebuy: bool = false; // Auto-rebuy when stack reaches 0
|
|
50
|
+
|
|
51
|
+
constructor(
|
|
52
|
+
allowed: bool = true,
|
|
53
|
+
cooldownMs: i64 = 0,
|
|
54
|
+
maxRebuys: i32 = -1,
|
|
55
|
+
autoRebuy: bool = false
|
|
56
|
+
) {
|
|
57
|
+
this.allowed = allowed;
|
|
58
|
+
this.cooldownMs = cooldownMs;
|
|
59
|
+
this.maxRebuys = maxRebuys;
|
|
60
|
+
this.autoRebuy = autoRebuy;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
clone(): RebuyConfig {
|
|
64
|
+
return new RebuyConfig(this.allowed, this.cooldownMs, this.maxRebuys, this.autoRebuy);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Cash game configuration
|
|
70
|
+
* Combines all cash game settings
|
|
71
|
+
*/
|
|
72
|
+
export class CashGameConfig {
|
|
73
|
+
buyInRange: BuyInRange = new BuyInRange();
|
|
74
|
+
rakeConfig: RakeConfig = new RakeConfig();
|
|
75
|
+
rebuyConfig: RebuyConfig = new RebuyConfig();
|
|
76
|
+
|
|
77
|
+
constructor(
|
|
78
|
+
buyInRange: BuyInRange | null = null,
|
|
79
|
+
rakeConfig: RakeConfig | null = null,
|
|
80
|
+
rebuyConfig: RebuyConfig | null = null
|
|
81
|
+
) {
|
|
82
|
+
this.buyInRange = buyInRange !== null ? buyInRange : new BuyInRange();
|
|
83
|
+
this.rakeConfig = rakeConfig !== null ? rakeConfig : new RakeConfig();
|
|
84
|
+
this.rebuyConfig = rebuyConfig !== null ? rebuyConfig : new RebuyConfig();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
clone(): CashGameConfig {
|
|
88
|
+
return new CashGameConfig(
|
|
89
|
+
this.buyInRange.clone(),
|
|
90
|
+
this.rakeConfig.clone(),
|
|
91
|
+
this.rebuyConfig.clone()
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Base interface for cash game seats
|
|
98
|
+
* Games should extend this with game-specific fields
|
|
99
|
+
*/
|
|
100
|
+
export class CashGameSeatBase {
|
|
101
|
+
seatId: i32 = 0;
|
|
102
|
+
playerId: string | null = null;
|
|
103
|
+
stack: string = "0"; // Current stack (BigInt string)
|
|
104
|
+
buyInAmount: string = "0"; // Original buy-in amount (BigInt string)
|
|
105
|
+
lastRebuyAt: i64 = 0; // Timestamp of last rebuy
|
|
106
|
+
rebuyCount: i32 = 0; // Number of rebuys this player has made
|
|
107
|
+
|
|
108
|
+
constructor(
|
|
109
|
+
seatId: i32 = 0,
|
|
110
|
+
playerId: string | null = null,
|
|
111
|
+
stack: string = "0",
|
|
112
|
+
buyInAmount: string = "0"
|
|
113
|
+
) {
|
|
114
|
+
this.seatId = seatId;
|
|
115
|
+
this.playerId = playerId;
|
|
116
|
+
this.stack = stack;
|
|
117
|
+
this.buyInAmount = buyInAmount;
|
|
118
|
+
this.lastRebuyAt = 0;
|
|
119
|
+
this.rebuyCount = 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
isEmpty(): bool {
|
|
123
|
+
const pid = this.playerId;
|
|
124
|
+
if (pid === null) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
return pid.length === 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
clone(): CashGameSeatBase {
|
|
131
|
+
const seat = new CashGameSeatBase(
|
|
132
|
+
this.seatId,
|
|
133
|
+
this.playerId,
|
|
134
|
+
this.stack,
|
|
135
|
+
this.buyInAmount
|
|
136
|
+
);
|
|
137
|
+
seat.lastRebuyAt = this.lastRebuyAt;
|
|
138
|
+
seat.rebuyCount = this.rebuyCount;
|
|
139
|
+
return seat;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Shared utilities for cash games
|
|
4
|
+
*
|
|
5
|
+
* Standardized functions for buy-in validation, rake calculation, rebuy eligibility, etc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { BigInt } from "@arcanahq/core/assembly/primitives/bigint";
|
|
9
|
+
import { is_valid_u256_string, parse_u256_string } from "@arcanahq/core/assembly/primitives/conversions";
|
|
10
|
+
import { CashGameSeatBase } from "./cash_game_types";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Validate buy-in amount is within min/max range
|
|
14
|
+
* @param amount Buy-in amount as string (BigInt)
|
|
15
|
+
* @param minBuyIn Minimum buy-in as string (BigInt)
|
|
16
|
+
* @param maxBuyIn Maximum buy-in as string (BigInt)
|
|
17
|
+
* @returns true if valid, false otherwise
|
|
18
|
+
*/
|
|
19
|
+
export function validateBuyInAmount(amount: string, minBuyIn: string, maxBuyIn: string): bool {
|
|
20
|
+
// Validate amount format
|
|
21
|
+
if (!is_valid_u256_string(amount)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const amountBigInt = parse_u256_string(amount);
|
|
26
|
+
if (amountBigInt.lte(BigInt.ZERO)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Validate min/max format
|
|
31
|
+
if (!is_valid_u256_string(minBuyIn) || !is_valid_u256_string(maxBuyIn)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const minBigInt = parse_u256_string(minBuyIn);
|
|
36
|
+
const maxBigInt = parse_u256_string(maxBuyIn);
|
|
37
|
+
|
|
38
|
+
// Validate range
|
|
39
|
+
if (minBigInt.lte(BigInt.ZERO) || maxBigInt.lte(BigInt.ZERO)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (minBigInt.gt(maxBigInt)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check amount is within range
|
|
48
|
+
if (amountBigInt.lt(minBigInt) || amountBigInt.gt(maxBigInt)) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Calculate rake for a pot amount
|
|
57
|
+
* @param potAmount Pot amount as i64
|
|
58
|
+
* @param rakePercentage Rake percentage (e.g., 5.0 for 5%)
|
|
59
|
+
* @param rakeCap Maximum rake per pot (0 = no cap)
|
|
60
|
+
* @returns Rake amount as i64
|
|
61
|
+
*/
|
|
62
|
+
export function calculateRake(potAmount: i64, rakePercentage: f64, rakeCap: i64): i64 {
|
|
63
|
+
if (rakePercentage <= 0.0) {
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Calculate rake as percentage
|
|
68
|
+
const rake = <i64>(<f64>potAmount * rakePercentage / 100.0);
|
|
69
|
+
|
|
70
|
+
// Apply cap if set
|
|
71
|
+
if (rakeCap > 0 && rake > rakeCap) {
|
|
72
|
+
return rakeCap;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return rake;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Calculate rake for a pot amount (BigInt string version)
|
|
80
|
+
* @param potAmount Pot amount as string (BigInt)
|
|
81
|
+
* @param rakePercentage Rake percentage (e.g., 5.0 for 5%)
|
|
82
|
+
* @param rakeCap Maximum rake per pot as string (BigInt, "0" = no cap)
|
|
83
|
+
* @returns Rake amount as string (BigInt)
|
|
84
|
+
*/
|
|
85
|
+
export function calculateRakeString(potAmount: string, rakePercentage: f64, rakeCap: string): string {
|
|
86
|
+
if (rakePercentage <= 0.0) {
|
|
87
|
+
return "0";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!is_valid_u256_string(potAmount)) {
|
|
91
|
+
return "0";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const potBigInt = parse_u256_string(potAmount);
|
|
95
|
+
if (potBigInt.lte(BigInt.ZERO)) {
|
|
96
|
+
return "0";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// For percentage calculation, we need to use fixed-point math
|
|
100
|
+
// Calculate percentage using BigInt operations
|
|
101
|
+
// potAmount * percentage / 100
|
|
102
|
+
// Since we can't do floating point with BigInt directly, we'll use integer math:
|
|
103
|
+
// Multiply by percentage (as integer * 100), then divide by 10000
|
|
104
|
+
// e.g., 5% = 500 / 10000
|
|
105
|
+
const percentageInt = <i64>(rakePercentage * 100.0); // e.g., 5.0% = 500
|
|
106
|
+
const percentageBigInt = BigInt.fromString(percentageInt.toString());
|
|
107
|
+
const divisorBigInt = BigInt.fromString("10000");
|
|
108
|
+
|
|
109
|
+
// Calculate: pot * percentage / 10000
|
|
110
|
+
const rakeBigInt = potBigInt.mul(percentageBigInt).div(divisorBigInt);
|
|
111
|
+
|
|
112
|
+
// Apply cap if set
|
|
113
|
+
if (is_valid_u256_string(rakeCap) && rakeCap !== "0") {
|
|
114
|
+
const capBigInt = parse_u256_string(rakeCap);
|
|
115
|
+
if (capBigInt.gt(BigInt.ZERO) && rakeBigInt.gt(capBigInt)) {
|
|
116
|
+
return capBigInt.toString();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return rakeBigInt.toString();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check if a player can rebuy
|
|
125
|
+
* @param seat Seat to check
|
|
126
|
+
* @param gamePhase Current game phase
|
|
127
|
+
* @param rebuyAllowed Whether rebuys are allowed
|
|
128
|
+
* @param cooldownMs Cooldown between rebuys
|
|
129
|
+
* @param nowMs Current timestamp in milliseconds
|
|
130
|
+
* @returns true if rebuy is allowed, false otherwise
|
|
131
|
+
*/
|
|
132
|
+
export function canRebuy(
|
|
133
|
+
seat: CashGameSeatBase,
|
|
134
|
+
gamePhase: string,
|
|
135
|
+
rebuyAllowed: bool,
|
|
136
|
+
cooldownMs: i64,
|
|
137
|
+
nowMs: i64
|
|
138
|
+
): bool {
|
|
139
|
+
if (!rebuyAllowed) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Must have a player
|
|
144
|
+
if (seat.isEmpty()) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check cooldown
|
|
149
|
+
if (cooldownMs > 0 && seat.lastRebuyAt > 0) {
|
|
150
|
+
const timeSinceRebuy = nowMs - seat.lastRebuyAt;
|
|
151
|
+
if (timeSinceRebuy < cooldownMs) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Check if stack is zero or very low (for rebuy eligibility)
|
|
157
|
+
const stackBigInt = parse_u256_string(seat.stack);
|
|
158
|
+
if (stackBigInt.gt(BigInt.ZERO)) {
|
|
159
|
+
// For top-up, not rebuy - use canTopUp instead
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Can rebuy if stack is zero and not in active hand
|
|
164
|
+
// Game-specific phases should be checked by caller
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check if a player can top up
|
|
170
|
+
* @param seat Seat to check
|
|
171
|
+
* @param topUpAmount Amount to top up as string (BigInt)
|
|
172
|
+
* @param maxBuyIn Maximum buy-in as string (BigInt)
|
|
173
|
+
* @param gamePhase Current game phase
|
|
174
|
+
* @returns true if top-up is allowed, false otherwise
|
|
175
|
+
*/
|
|
176
|
+
export function canTopUp(
|
|
177
|
+
seat: CashGameSeatBase,
|
|
178
|
+
topUpAmount: string,
|
|
179
|
+
maxBuyIn: string,
|
|
180
|
+
gamePhase: string
|
|
181
|
+
): bool {
|
|
182
|
+
// Must have a player
|
|
183
|
+
if (seat.isEmpty()) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Validate top-up amount
|
|
188
|
+
if (!is_valid_u256_string(topUpAmount)) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const topUpBigInt = parse_u256_string(topUpAmount);
|
|
193
|
+
if (topUpBigInt.lte(BigInt.ZERO)) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check current stack + top-up doesn't exceed max buy-in
|
|
198
|
+
const stackBigInt = parse_u256_string(seat.stack);
|
|
199
|
+
const maxBigInt = parse_u256_string(maxBuyIn);
|
|
200
|
+
|
|
201
|
+
const newStack = stackBigInt.add(topUpBigInt);
|
|
202
|
+
if (newStack.gt(maxBigInt)) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Can top up if not in active hand
|
|
207
|
+
// Game-specific phases should be checked by caller
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Initialize cash game seats array
|
|
213
|
+
* @param maxSeats Maximum number of seats
|
|
214
|
+
* @returns Array of empty seats with seat IDs 0 to maxSeats-1
|
|
215
|
+
*/
|
|
216
|
+
export function initializeCashGameSeats(maxSeats: i32): CashGameSeatBase[] {
|
|
217
|
+
const seats = new Array<CashGameSeatBase>(maxSeats);
|
|
218
|
+
for (let i = 0; i < maxSeats; i++) {
|
|
219
|
+
seats[i] = new CashGameSeatBase(i, null, "0", "0");
|
|
220
|
+
}
|
|
221
|
+
return seats;
|
|
222
|
+
}
|
|
223
|
+
|