texasholdem 1.0.0 → 1.0.3
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/.github/workflows/auto-tag.yml +73 -0
- package/.github/workflows/ci.yml +28 -0
- package/.github/workflows/publish.yml +11 -9
- package/README.md +186 -5
- package/eslint.config.js +11 -0
- package/package.json +5 -1
- package/src/action.ts +15 -2
- package/src/betting.ts +1 -1
- package/src/brand.ts +5 -5
- package/src/card.ts +5 -3
- package/src/deck.ts +1 -1
- package/src/event.ts +1 -1
- package/src/hand.ts +1 -2
- package/src/index.ts +20 -0
- package/src/loop.ts +344 -0
- package/src/position.ts +310 -0
- package/src/table.ts +11 -16
- package/test/betting.test.ts +1 -1
- package/test/deck.test.ts +4 -6
- package/test/demo.test.ts +202 -0
- package/test/loop.test.ts +251 -0
- package/test/position.test.ts +240 -0
- package/test/properties/action.properties.ts +0 -1
- package/test/properties/betting.properties.ts +1 -1
- package/test/properties/deck.properties.ts +1 -2
- package/test/properties/hand.properties.ts +0 -1
- package/test/properties/player.properties.ts +2 -2
- package/test/properties/pot.properties.ts +0 -1
- package/test/properties/table.properties.ts +1 -13
- package/.claude/settings.local.json +0 -14
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
name: Auto-tag & publish on version bump
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
paths: [package.json]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
tag:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: write
|
|
13
|
+
outputs:
|
|
14
|
+
changed: ${{ steps.version.outputs.changed }}
|
|
15
|
+
tag: ${{ steps.version.outputs.tag }}
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
with:
|
|
19
|
+
fetch-depth: 2
|
|
20
|
+
|
|
21
|
+
- name: Check if version changed
|
|
22
|
+
id: version
|
|
23
|
+
run: |
|
|
24
|
+
NEW=$(node -p "require('./package.json').version")
|
|
25
|
+
git checkout HEAD~1 -- package.json 2>/dev/null
|
|
26
|
+
OLD=$(node -p "require('./package.json').version")
|
|
27
|
+
git checkout HEAD -- package.json
|
|
28
|
+
if [ "$OLD" != "$NEW" ]; then
|
|
29
|
+
echo "changed=true" >> "$GITHUB_OUTPUT"
|
|
30
|
+
echo "tag=v$NEW" >> "$GITHUB_OUTPUT"
|
|
31
|
+
echo "Version changed: $OLD -> $NEW"
|
|
32
|
+
else
|
|
33
|
+
echo "changed=false" >> "$GITHUB_OUTPUT"
|
|
34
|
+
echo "Version unchanged: $OLD"
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
- name: Create and push tag
|
|
38
|
+
if: steps.version.outputs.changed == 'true'
|
|
39
|
+
run: |
|
|
40
|
+
git tag ${{ steps.version.outputs.tag }}
|
|
41
|
+
git push origin ${{ steps.version.outputs.tag }}
|
|
42
|
+
|
|
43
|
+
publish:
|
|
44
|
+
needs: tag
|
|
45
|
+
if: needs.tag.outputs.changed == 'true'
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
environment: production
|
|
48
|
+
permissions:
|
|
49
|
+
id-token: write
|
|
50
|
+
contents: read
|
|
51
|
+
steps:
|
|
52
|
+
- uses: actions/checkout@v4
|
|
53
|
+
|
|
54
|
+
- uses: pnpm/action-setup@v4
|
|
55
|
+
with:
|
|
56
|
+
version: 9
|
|
57
|
+
|
|
58
|
+
- uses: actions/setup-node@v4
|
|
59
|
+
with:
|
|
60
|
+
node-version: "24"
|
|
61
|
+
registry-url: "https://registry.npmjs.org"
|
|
62
|
+
cache: pnpm
|
|
63
|
+
|
|
64
|
+
- run: npm install -g npm@latest
|
|
65
|
+
- run: pnpm install --frozen-lockfile
|
|
66
|
+
- run: pnpm typecheck
|
|
67
|
+
- run: pnpm test
|
|
68
|
+
- run: pnpm build
|
|
69
|
+
|
|
70
|
+
- name: Publish with OIDC
|
|
71
|
+
run: npm publish --provenance --access public
|
|
72
|
+
env:
|
|
73
|
+
NODE_AUTH_TOKEN: ""
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: [master]
|
|
6
|
+
push:
|
|
7
|
+
branches: [master]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
check:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- uses: pnpm/action-setup@v4
|
|
16
|
+
with:
|
|
17
|
+
version: 9
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-node@v4
|
|
20
|
+
with:
|
|
21
|
+
node-version: "24"
|
|
22
|
+
cache: pnpm
|
|
23
|
+
|
|
24
|
+
- run: pnpm install --frozen-lockfile
|
|
25
|
+
- run: pnpm typecheck
|
|
26
|
+
- run: pnpm lint
|
|
27
|
+
- run: pnpm test
|
|
28
|
+
- run: pnpm build
|
|
@@ -5,12 +5,14 @@ on:
|
|
|
5
5
|
tags:
|
|
6
6
|
- "v*"
|
|
7
7
|
|
|
8
|
+
permissions:
|
|
9
|
+
id-token: write
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
8
12
|
jobs:
|
|
9
13
|
publish:
|
|
10
14
|
runs-on: ubuntu-latest
|
|
11
|
-
|
|
12
|
-
contents: read
|
|
13
|
-
id-token: write
|
|
15
|
+
environment: production
|
|
14
16
|
steps:
|
|
15
17
|
- uses: actions/checkout@v4
|
|
16
18
|
|
|
@@ -20,17 +22,17 @@ jobs:
|
|
|
20
22
|
|
|
21
23
|
- uses: actions/setup-node@v4
|
|
22
24
|
with:
|
|
23
|
-
node-version:
|
|
25
|
+
node-version: "24"
|
|
26
|
+
registry-url: "https://registry.npmjs.org"
|
|
24
27
|
cache: pnpm
|
|
25
|
-
registry-url: https://registry.npmjs.org
|
|
26
28
|
|
|
29
|
+
- run: npm install -g npm@latest
|
|
27
30
|
- run: pnpm install --frozen-lockfile
|
|
28
31
|
- run: pnpm typecheck
|
|
29
32
|
- run: pnpm test
|
|
30
33
|
- run: pnpm build
|
|
31
34
|
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
npm publish --provenance --access public
|
|
35
|
+
- name: Publish with OIDC
|
|
36
|
+
run: npm publish --provenance --access public
|
|
35
37
|
env:
|
|
36
|
-
NODE_AUTH_TOKEN:
|
|
38
|
+
NODE_AUTH_TOKEN: ""
|
package/README.md
CHANGED
|
@@ -28,6 +28,58 @@ pnpm add texasholdem
|
|
|
28
28
|
|
|
29
29
|
## Quick Start
|
|
30
30
|
|
|
31
|
+
### Strategy-based game loop (recommended)
|
|
32
|
+
|
|
33
|
+
The simplest way to run poker hands — define a strategy function and let the engine drive the loop:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { Effect, Either } from "effect";
|
|
37
|
+
import {
|
|
38
|
+
Chips,
|
|
39
|
+
SeatIndex,
|
|
40
|
+
createTable,
|
|
41
|
+
sitDown,
|
|
42
|
+
playGame,
|
|
43
|
+
fromSync,
|
|
44
|
+
stopAfterHands,
|
|
45
|
+
Check,
|
|
46
|
+
Call,
|
|
47
|
+
Fold,
|
|
48
|
+
} from "texasholdem";
|
|
49
|
+
|
|
50
|
+
// 1. Create a table and seat players
|
|
51
|
+
let table = Either.getOrThrow(
|
|
52
|
+
createTable({
|
|
53
|
+
maxSeats: 6,
|
|
54
|
+
forcedBets: { smallBlind: Chips(5), bigBlind: Chips(10) },
|
|
55
|
+
})
|
|
56
|
+
);
|
|
57
|
+
for (const i of [0, 1, 2, 3]) {
|
|
58
|
+
table = Either.getOrThrow(sitDown(table, SeatIndex(i), Chips(1000)));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 2. Define a strategy — receives full positional context
|
|
62
|
+
const myStrategy = fromSync((ctx) => {
|
|
63
|
+
if (ctx.legalActions.canCheck) return Check;
|
|
64
|
+
return Call;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// 3. Run 100 hands
|
|
68
|
+
const result = Effect.runSync(
|
|
69
|
+
playGame(myStrategy, {
|
|
70
|
+
stopWhen: stopAfterHands(100),
|
|
71
|
+
onEvent: (ev) => console.log(ev._tag),
|
|
72
|
+
defaultAction: Fold,
|
|
73
|
+
})(table)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
console.log(`Played ${result.handsPlayed} hands`);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Manual loop (full control)
|
|
80
|
+
|
|
81
|
+
For UI-driven games, bots with external I/O, or when you need to control each action individually:
|
|
82
|
+
|
|
31
83
|
```typescript
|
|
32
84
|
import { Effect, Either, Option } from "effect";
|
|
33
85
|
import {
|
|
@@ -39,9 +91,8 @@ import {
|
|
|
39
91
|
tableAct,
|
|
40
92
|
getActivePlayer,
|
|
41
93
|
getTableLegalActions,
|
|
42
|
-
Call,
|
|
43
|
-
Fold,
|
|
44
94
|
Check,
|
|
95
|
+
Call,
|
|
45
96
|
} from "texasholdem";
|
|
46
97
|
|
|
47
98
|
// 1. Create a table
|
|
@@ -77,7 +128,7 @@ console.log(`Hand finished. Events: ${state.events.length}`);
|
|
|
77
128
|
|
|
78
129
|
## Architecture
|
|
79
130
|
|
|
80
|
-
|
|
131
|
+
14 modules in strict bottom-up dependency order:
|
|
81
132
|
|
|
82
133
|
```
|
|
83
134
|
brand.ts ─── card.ts ─── deck.ts ───────────────────┐
|
|
@@ -93,8 +144,12 @@ brand.ts ─── card.ts ─── deck.ts ───────────
|
|
|
93
144
|
│ hand.ts ────────────────────────┘
|
|
94
145
|
│ │
|
|
95
146
|
└───────────────── table.ts
|
|
96
|
-
|
|
97
|
-
|
|
147
|
+
│ │
|
|
148
|
+
position.ts │
|
|
149
|
+
│ │
|
|
150
|
+
loop.ts
|
|
151
|
+
│
|
|
152
|
+
index.ts (barrel exports)
|
|
98
153
|
```
|
|
99
154
|
|
|
100
155
|
### Module Summary
|
|
@@ -113,9 +168,99 @@ brand.ts ─── card.ts ─── deck.ts ───────────
|
|
|
113
168
|
| `betting` | Betting round state machine: turn order, completion detection, action validation |
|
|
114
169
|
| `hand` | Full hand lifecycle: Preflop → Flop → Turn → River → Showdown → Complete |
|
|
115
170
|
| `table` | Multi-hand session: seating, button movement, busted player removal |
|
|
171
|
+
| `position` | Positional roles (`Button`, `UTG`, `CO`, …) and `StrategyContext` builder |
|
|
172
|
+
| `loop` | Strategy-driven game loop: `playHand`, `playGame`, timeout/fallback handling |
|
|
116
173
|
|
|
117
174
|
## API Overview
|
|
118
175
|
|
|
176
|
+
### Game Loop (strategy-driven)
|
|
177
|
+
|
|
178
|
+
The highest-level API — define a strategy function and the engine handles dealing, betting rounds, phase advancement, and multi-hand sessions automatically.
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// Strategy receives full context, returns an action
|
|
182
|
+
type Strategy = (ctx: StrategyContext) => Effect.Effect<Action>
|
|
183
|
+
type SyncStrategy = (ctx: StrategyContext) => Action
|
|
184
|
+
|
|
185
|
+
// Wrap a synchronous function as a Strategy
|
|
186
|
+
fromSync(fn: SyncStrategy): Strategy
|
|
187
|
+
|
|
188
|
+
// Drive a single hand (table must already have a hand started)
|
|
189
|
+
playOneHand(strategy, opts?): (state: TableState) => Effect<PlayHandResult, PokerError>
|
|
190
|
+
|
|
191
|
+
// Start + drive a single hand
|
|
192
|
+
playHand(strategy, opts?): (state: TableState) => Effect<PlayHandResult, PokerError>
|
|
193
|
+
|
|
194
|
+
// Multi-hand loop — keeps dealing until a stop condition is met
|
|
195
|
+
playGame(strategy, opts?): (state: TableState) => Effect<PlayGameResult, PokerError>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Options:**
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
interface PlayHandOptions {
|
|
202
|
+
actionTimeout?: Duration.DurationInput // e.g. "5 seconds"
|
|
203
|
+
defaultAction?: Action | ((ctx: StrategyContext) => Action)
|
|
204
|
+
onEvent?: (event: GameEvent) => void
|
|
205
|
+
maxActionsPerHand?: number // default 500 (safety circuit breaker)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
interface PlayGameOptions extends PlayHandOptions {
|
|
209
|
+
stopWhen?: StopCondition
|
|
210
|
+
maxHands?: number // default 10_000
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Built-in stop conditions:**
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
stopAfterHands(n: number): StopCondition
|
|
218
|
+
stopWhenFewPlayers(min?: number): StopCondition // default min = 2
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Built-in strategies:**
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
alwaysFold: Strategy // folds every hand
|
|
225
|
+
passiveStrategy: Strategy // checks when possible, calls otherwise
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Resilience:** when a strategy returns an invalid action, the engine applies a three-level fallback: (1) the returned action, (2) `defaultAction` if provided, (3) Check > Call > Fold. Strategies never need to be defensive about illegal moves.
|
|
229
|
+
|
|
230
|
+
### Strategy Context
|
|
231
|
+
|
|
232
|
+
Every strategy call receives a `StrategyContext` — everything a decision-maker needs:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
interface StrategyContext {
|
|
236
|
+
// Identity
|
|
237
|
+
seat: SeatIndex
|
|
238
|
+
chips: Chips
|
|
239
|
+
holeCards: Option<readonly [Card, Card]>
|
|
240
|
+
|
|
241
|
+
// Position
|
|
242
|
+
role: PositionalRole // "Button" | "SmallBlind" | "BigBlind" | "UTG" | "UTG1" | "UTG2" | "LJ" | "HJ" | "CO"
|
|
243
|
+
buttonSeat: SeatIndex
|
|
244
|
+
smallBlindSeat: SeatIndex
|
|
245
|
+
bigBlindSeat: SeatIndex
|
|
246
|
+
playersToActAfter: number
|
|
247
|
+
|
|
248
|
+
// Hand state
|
|
249
|
+
phase: Phase // "Preflop" | "Flop" | "Turn" | "River" | "Showdown" | "Complete"
|
|
250
|
+
communityCards: Card[]
|
|
251
|
+
potTotal: Chips
|
|
252
|
+
bigBlind: Chips
|
|
253
|
+
activeSeatCount: number // non-folded, non-busted players
|
|
254
|
+
|
|
255
|
+
// Action
|
|
256
|
+
legalActions: LegalActions
|
|
257
|
+
players: PlayerView[] // all players visible state
|
|
258
|
+
newEvents: GameEvent[] // events since your last action
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Positional roles are assigned automatically based on player count (2–10), following standard poker conventions (heads-up: Button = SB).
|
|
263
|
+
|
|
119
264
|
### Table-Level (multi-hand sessions)
|
|
120
265
|
|
|
121
266
|
```typescript
|
|
@@ -130,6 +275,8 @@ getTableLegalActions(state): Option<LegalActions>
|
|
|
130
275
|
|
|
131
276
|
### Hand-Level (single hand)
|
|
132
277
|
|
|
278
|
+
Lower-level API for controlling a single hand directly. Most users should prefer the Table or Game Loop API.
|
|
279
|
+
|
|
133
280
|
```typescript
|
|
134
281
|
startHand(players, button, forcedBets, handId): Effect<HandState, PokerError>
|
|
135
282
|
act(state, seat, action): Either<HandState, PokerError>
|
|
@@ -151,6 +298,39 @@ Raise({ amount }) // Raise over current bet (branded Chips)
|
|
|
151
298
|
AllIn // Put all remaining chips in
|
|
152
299
|
```
|
|
153
300
|
|
|
301
|
+
### LegalActions
|
|
302
|
+
|
|
303
|
+
Tells a strategy what moves are currently valid:
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
interface LegalActions {
|
|
307
|
+
canFold: boolean
|
|
308
|
+
canCheck: boolean
|
|
309
|
+
callAmount: Option<Chips> // None = no bet to match
|
|
310
|
+
minBet: Option<Chips> // available when no prior bet (opening aggression)
|
|
311
|
+
maxBet: Option<Chips>
|
|
312
|
+
minRaise: Option<Chips> // available when a bet already exists
|
|
313
|
+
maxRaise: Option<Chips>
|
|
314
|
+
canAllIn: boolean
|
|
315
|
+
allInAmount: Chips
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Events
|
|
320
|
+
|
|
321
|
+
Every state change is recorded as a `GameEvent` — a full hand history / audit log:
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
type GameEvent =
|
|
325
|
+
| HandStarted | BlindsPosted | HoleCardsDealt
|
|
326
|
+
| PlayerActed | BettingRoundEnded
|
|
327
|
+
| CommunityCardsDealt | ShowdownStarted
|
|
328
|
+
| PotAwarded | HandEnded
|
|
329
|
+
| PlayerSatDown | PlayerStoodUp
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Use `state.events` for table-level events, or the `onEvent` callback in the game loop for real-time streaming.
|
|
333
|
+
|
|
154
334
|
## Effect-TS Usage
|
|
155
335
|
|
|
156
336
|
| Where | What | Why |
|
|
@@ -158,6 +338,7 @@ AllIn // Put all remaining chips in
|
|
|
158
338
|
| `deck.ts` shuffle | `Effect<Deck>` | Randomness is a side effect |
|
|
159
339
|
| `hand.ts` startHand | `Effect<HandState, PokerError>` | Calls shuffle |
|
|
160
340
|
| `table.ts` startNextHand | `Effect<TableState, PokerError>` | Calls startHand |
|
|
341
|
+
| `loop.ts` playHand / playGame | `Effect<Result, PokerError>` | Orchestrates effectful hand starts + strategy calls |
|
|
161
342
|
| Everything else | Pure functions / `Either` | No side effects needed |
|
|
162
343
|
| Branded types | `Brand.refined` | Compile-time + runtime safety |
|
|
163
344
|
| Errors | `Data.TaggedError` | Pattern-matchable typed errors |
|
package/eslint.config.js
CHANGED
|
@@ -49,9 +49,20 @@ export default tseslint.config(
|
|
|
49
49
|
"@typescript-eslint/no-empty-object-type": "off",
|
|
50
50
|
// Allow non-null assertions where array bounds are checked manually
|
|
51
51
|
"@typescript-eslint/no-non-null-assertion": "warn",
|
|
52
|
+
// Ban type assertions (as Type) — use type narrowing instead
|
|
53
|
+
"@typescript-eslint/consistent-type-assertions": [
|
|
54
|
+
"error",
|
|
55
|
+
{ assertionStyle: "never" },
|
|
56
|
+
],
|
|
52
57
|
},
|
|
53
58
|
},
|
|
54
59
|
|
|
60
|
+
// Disable type-checked rules for test files (not in tsconfig project)
|
|
61
|
+
{
|
|
62
|
+
files: ["test/**/*.ts"],
|
|
63
|
+
...tseslint.configs.disableTypeChecked,
|
|
64
|
+
},
|
|
65
|
+
|
|
55
66
|
// Prettier — must be last to disable conflicting formatting rules
|
|
56
67
|
prettierConfig,
|
|
57
68
|
);
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "texasholdem",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Modular, functional Texas Hold'em poker engine built with Effect-TS",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/eliraz-refael/texasHoldem-ts.git"
|
|
9
|
+
},
|
|
6
10
|
"main": "./dist/index.js",
|
|
7
11
|
"types": "./dist/index.d.ts",
|
|
8
12
|
"scripts": {
|
package/src/action.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { Data, Either, Match, Option, pipe } from "effect";
|
|
7
|
+
import { Data, Either, Match, Option, Schema, pipe } from "effect";
|
|
8
8
|
import type { Chips } from "./brand.js";
|
|
9
|
-
import { Chips as makeChips, chipsToNumber } from "./brand.js";
|
|
9
|
+
import { Chips as makeChips, chipsToNumber, ChipsSchema } from "./brand.js";
|
|
10
10
|
import { InvalidAction } from "./error.js";
|
|
11
11
|
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
@@ -49,6 +49,19 @@ export interface LegalActions {
|
|
|
49
49
|
readonly allInAmount: Chips;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/** Schema for LegalActions — enables composition in StrategyContextSchema and Arbitrary generation. */
|
|
53
|
+
export const LegalActionsSchema = Schema.Struct({
|
|
54
|
+
canFold: Schema.Boolean,
|
|
55
|
+
canCheck: Schema.Boolean,
|
|
56
|
+
callAmount: Schema.Option(ChipsSchema),
|
|
57
|
+
minBet: Schema.Option(ChipsSchema),
|
|
58
|
+
maxBet: Schema.Option(ChipsSchema),
|
|
59
|
+
minRaise: Schema.Option(ChipsSchema),
|
|
60
|
+
maxRaise: Schema.Option(ChipsSchema),
|
|
61
|
+
canAllIn: Schema.Boolean,
|
|
62
|
+
allInAmount: ChipsSchema,
|
|
63
|
+
});
|
|
64
|
+
|
|
52
65
|
// ---------------------------------------------------------------------------
|
|
53
66
|
// computeLegalActions
|
|
54
67
|
// ---------------------------------------------------------------------------
|
package/src/betting.ts
CHANGED
|
@@ -194,7 +194,7 @@ export function applyAction(
|
|
|
194
194
|
const legal = getLegalActions(state);
|
|
195
195
|
const validated = validateAction(action, legal);
|
|
196
196
|
if (Either.isLeft(validated)) {
|
|
197
|
-
return
|
|
197
|
+
return Either.left(validated.left);
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
const player = getPlayer(state, seat);
|
package/src/brand.ts
CHANGED
|
@@ -51,18 +51,18 @@ export const ZERO_CHIPS: Chips = Chips(0);
|
|
|
51
51
|
|
|
52
52
|
/** Add two Chips values. */
|
|
53
53
|
export const addChips = (a: Chips, b: Chips): Chips =>
|
|
54
|
-
Chips(
|
|
54
|
+
Chips(a + b);
|
|
55
55
|
|
|
56
56
|
/** Subtract `b` from `a`. Caller must ensure `a >= b`. */
|
|
57
57
|
export const subtractChips = (a: Chips, b: Chips): Chips =>
|
|
58
|
-
Chips(
|
|
58
|
+
Chips(a - b);
|
|
59
59
|
|
|
60
60
|
/** Return the smaller of two Chips values. */
|
|
61
61
|
export const minChips = (a: Chips, b: Chips): Chips =>
|
|
62
|
-
|
|
62
|
+
a <= b ? a : b;
|
|
63
63
|
|
|
64
64
|
/** Unwrap a Chips value to a plain number. */
|
|
65
|
-
export const chipsToNumber = (c: Chips): number => c
|
|
65
|
+
export const chipsToNumber = (c: Chips): number => c;
|
|
66
66
|
|
|
67
67
|
/** Order instance for Chips (ascending by numeric value). */
|
|
68
68
|
export const ChipsOrder: Order.Order<Chips> = Order.mapInput(
|
|
@@ -106,7 +106,7 @@ export const SeatIndexSchema = Schema.Number.pipe(
|
|
|
106
106
|
);
|
|
107
107
|
|
|
108
108
|
/** Unwrap a SeatIndex value to a plain number. */
|
|
109
|
-
export const seatIndexToNumber = (s: SeatIndex): number => s
|
|
109
|
+
export const seatIndexToNumber = (s: SeatIndex): number => s;
|
|
110
110
|
|
|
111
111
|
/** Order instance for SeatIndex (ascending by numeric value). */
|
|
112
112
|
export const SeatIndexOrder: Order.Order<SeatIndex> = Order.mapInput(
|
package/src/card.ts
CHANGED
|
@@ -93,7 +93,9 @@ const CHAR_TO_RANK: Record<string, Rank> = {
|
|
|
93
93
|
A: 14,
|
|
94
94
|
};
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
function isSuit(s: string): s is Suit {
|
|
97
|
+
return SUITS.some((suit) => suit === s);
|
|
98
|
+
}
|
|
97
99
|
|
|
98
100
|
/**
|
|
99
101
|
* Convert a Card to its two-character pokersolver string.
|
|
@@ -139,7 +141,7 @@ export const cardFromString = (s: string): Either.Either<Card, InvalidCard> => {
|
|
|
139
141
|
);
|
|
140
142
|
}
|
|
141
143
|
|
|
142
|
-
if (!
|
|
144
|
+
if (!isSuit(suitChar)) {
|
|
143
145
|
return Either.left(
|
|
144
146
|
new InvalidCard({
|
|
145
147
|
input: s,
|
|
@@ -148,7 +150,7 @@ export const cardFromString = (s: string): Either.Either<Card, InvalidCard> => {
|
|
|
148
150
|
);
|
|
149
151
|
}
|
|
150
152
|
|
|
151
|
-
return Either.right(card(rank, suitChar
|
|
153
|
+
return Either.right(card(rank, suitChar));
|
|
152
154
|
};
|
|
153
155
|
|
|
154
156
|
/**
|
package/src/deck.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Array as A, Chunk, Effect, Either, HashMap, Random
|
|
1
|
+
import { Array as A, Chunk, Effect, Either, HashMap, Random } from "effect";
|
|
2
2
|
import type { Card } from "./card.js";
|
|
3
3
|
import { ALL_CARDS } from "./card.js";
|
|
4
4
|
import type { SeatIndex } from "./brand.js";
|
package/src/event.ts
CHANGED
|
@@ -18,7 +18,7 @@ import type { Action } from "./action.js";
|
|
|
18
18
|
// ---------------------------------------------------------------------------
|
|
19
19
|
|
|
20
20
|
export type GameEvent = Data.TaggedEnum<{
|
|
21
|
-
HandStarted: { readonly handId: HandId; readonly button: SeatIndex; readonly players: readonly SeatIndex[] };
|
|
21
|
+
HandStarted: { readonly handId: HandId; readonly button: SeatIndex; readonly smallBlind: SeatIndex; readonly bigBlind: SeatIndex; readonly players: readonly SeatIndex[] };
|
|
22
22
|
BlindsPosted: { readonly smallBlind: { readonly seat: SeatIndex; readonly amount: Chips }; readonly bigBlind: { readonly seat: SeatIndex; readonly amount: Chips } };
|
|
23
23
|
HoleCardsDealt: { readonly seat: SeatIndex };
|
|
24
24
|
PlayerActed: { readonly seat: SeatIndex; readonly action: Action };
|
package/src/hand.ts
CHANGED
|
@@ -10,7 +10,6 @@ import type { Chips, SeatIndex, HandId } from "./brand.js";
|
|
|
10
10
|
import {
|
|
11
11
|
ZERO_CHIPS,
|
|
12
12
|
minChips,
|
|
13
|
-
chipsToNumber,
|
|
14
13
|
SeatIndexOrder,
|
|
15
14
|
} from "./brand.js";
|
|
16
15
|
import type { Card } from "./card.js";
|
|
@@ -234,7 +233,7 @@ export function startHand(
|
|
|
234
233
|
const bbAmount = minChips(forcedBets.bigBlind, bbPlayer.chips);
|
|
235
234
|
currentPlayers = updatePlayer(currentPlayers, bbSeat, (p) => placeBet(p, bbAmount));
|
|
236
235
|
|
|
237
|
-
events.push(HandStarted({ handId, button, players: seatOrder }));
|
|
236
|
+
events.push(HandStarted({ handId, button, smallBlind: sbSeat, bigBlind: bbSeat, players: seatOrder }));
|
|
238
237
|
events.push(
|
|
239
238
|
BlindsPosted({
|
|
240
239
|
smallBlind: { seat: sbSeat, amount: sbAmount },
|
package/src/index.ts
CHANGED
|
@@ -33,3 +33,23 @@ export {
|
|
|
33
33
|
getActivePlayer,
|
|
34
34
|
getTableLegalActions,
|
|
35
35
|
} from "./table.js";
|
|
36
|
+
|
|
37
|
+
export * from "./position.js";
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
type Strategy,
|
|
41
|
+
type SyncStrategy,
|
|
42
|
+
type StopCondition,
|
|
43
|
+
type PlayHandOptions,
|
|
44
|
+
type PlayHandResult,
|
|
45
|
+
type PlayGameOptions,
|
|
46
|
+
type PlayGameResult,
|
|
47
|
+
fromSync,
|
|
48
|
+
playOneHand,
|
|
49
|
+
playHand,
|
|
50
|
+
playGame,
|
|
51
|
+
stopAfterHands,
|
|
52
|
+
stopWhenFewPlayers,
|
|
53
|
+
alwaysFold,
|
|
54
|
+
passiveStrategy,
|
|
55
|
+
} from "./loop.js";
|