texasholdem 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.
Files changed (46) hide show
  1. package/.claude/settings.local.json +14 -0
  2. package/.editorconfig +15 -0
  3. package/.github/workflows/publish.yml +36 -0
  4. package/.prettierrc +10 -0
  5. package/PLAN.md +215 -0
  6. package/README.md +182 -0
  7. package/eslint.config.js +57 -0
  8. package/package.json +36 -0
  9. package/src/action.ts +209 -0
  10. package/src/betting.ts +330 -0
  11. package/src/brand.ts +140 -0
  12. package/src/card.ts +179 -0
  13. package/src/deck.ts +155 -0
  14. package/src/error.ts +127 -0
  15. package/src/evaluator.ts +94 -0
  16. package/src/event.ts +48 -0
  17. package/src/hand.ts +609 -0
  18. package/src/index.ts +35 -0
  19. package/src/player.ts +96 -0
  20. package/src/pokersolver.d.ts +15 -0
  21. package/src/pot.ts +243 -0
  22. package/src/table.ts +300 -0
  23. package/test/action.test.ts +86 -0
  24. package/test/arbitraries.ts +74 -0
  25. package/test/betting.test.ts +139 -0
  26. package/test/brand.test.ts +18 -0
  27. package/test/card.test.ts +60 -0
  28. package/test/deck.test.ts +99 -0
  29. package/test/evaluator.test.ts +142 -0
  30. package/test/hand.test.ts +161 -0
  31. package/test/integration.test.ts +354 -0
  32. package/test/player.test.ts +21 -0
  33. package/test/pot.test.ts +192 -0
  34. package/test/properties/action.properties.ts +251 -0
  35. package/test/properties/betting.properties.ts +331 -0
  36. package/test/properties/brand.properties.ts +103 -0
  37. package/test/properties/card.properties.ts +100 -0
  38. package/test/properties/deck.properties.ts +160 -0
  39. package/test/properties/evaluator.properties.ts +119 -0
  40. package/test/properties/hand.properties.ts +357 -0
  41. package/test/properties/player.properties.ts +136 -0
  42. package/test/properties/pot.properties.ts +140 -0
  43. package/test/properties/table.properties.ts +377 -0
  44. package/test/table.test.ts +181 -0
  45. package/tsconfig.json +21 -0
  46. package/vitest.config.ts +8 -0
@@ -0,0 +1,14 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "WebFetch(domain:github.com)",
5
+ "WebFetch(domain:raw.githubusercontent.com)",
6
+ "WebFetch(domain:cdn.jsdelivr.net)",
7
+ "Bash(pnpm config:*)",
8
+ "Bash(pnpm add:*)",
9
+ "Bash(wc:*)",
10
+ "Bash(pnpm test:*)",
11
+ "Bash(pnpm tsc:*)"
12
+ ]
13
+ }
14
+ }
package/.editorconfig ADDED
@@ -0,0 +1,15 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_style = space
5
+ indent_size = 2
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
10
+
11
+ [*.md]
12
+ trim_trailing_whitespace = false
13
+
14
+ [Makefile]
15
+ indent_style = tab
@@ -0,0 +1,36 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: read
13
+ id-token: write
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: pnpm/action-setup@v4
18
+ with:
19
+ version: 9
20
+
21
+ - uses: actions/setup-node@v4
22
+ with:
23
+ node-version: 22
24
+ cache: pnpm
25
+ registry-url: https://registry.npmjs.org
26
+
27
+ - run: pnpm install --frozen-lockfile
28
+ - run: pnpm typecheck
29
+ - run: pnpm test
30
+ - run: pnpm build
31
+
32
+ - run: |
33
+ echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> .npmrc
34
+ npm publish --provenance --access public
35
+ env:
36
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/.prettierrc ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "semi": true,
3
+ "singleQuote": false,
4
+ "printWidth": 100,
5
+ "trailingComma": "all",
6
+ "tabWidth": 2,
7
+ "useTabs": false,
8
+ "bracketSpacing": true,
9
+ "arrowParens": "always"
10
+ }
package/PLAN.md ADDED
@@ -0,0 +1,215 @@
1
+ # Texas Hold'em Engine - Implementation Plan
2
+
3
+ ## Context
4
+
5
+ Build a modular, functional Texas Hold'em poker engine in TypeScript using Effect-TS. Inspired by [poker-ts](https://github.com/claudijo/poker-ts) but improved: immutable state, typed errors, event logging, and composable modules. Hand evaluation delegated to [pokersolver](https://github.com/goldfire/pokersolver) (battle-tested, production-proven) behind a clean abstraction boundary.
6
+
7
+ **Key improvements over poker-ts:**
8
+ - Immutable state transitions (`(state, action) => newState`) instead of mutation
9
+ - Typed errors via `Data.TaggedError` instead of thrown strings
10
+ - Event log accumulated in state for observability/hand history
11
+ - Automatic phase advancement (no manual `endBettingRound()` calls)
12
+ - Branded types (`Chips`, `SeatIndex`) prevent mixing up numbers at compile time
13
+ - Configurable seat count (poker-ts hardcodes 9)
14
+
15
+ ---
16
+
17
+ ## Module Architecture
18
+
19
+ 12 source files, strict bottom-up dependency order. Each module is independently usable.
20
+
21
+ ```
22
+ brand.ts ─── card.ts ─── deck.ts ───────────────────┐
23
+ │ │ │
24
+ │ └── evaluator.ts (pokersolver wrap) │
25
+ │ │
26
+ ├── player.ts ── action.ts ── event.ts │
27
+ │ │ │ │
28
+ │ └── pot.ts ──┤ │
29
+ │ │ │ │
30
+ │ betting.ts ──┘ │
31
+ │ │ │
32
+ │ hand.ts ────────────────────────┘
33
+ │ │
34
+ └───────────────── table.ts
35
+
36
+ index.ts (barrel exports)
37
+ ```
38
+
39
+ ---
40
+
41
+ ## Module Details
42
+
43
+ ### 1. `brand.ts` — Branded domain types
44
+ - `Chips` — non-negative integer (runtime validated via `Brand.refined`)
45
+ - `SeatIndex` — valid seat number
46
+ - `HandId` — unique hand identifier
47
+ - Schema counterparts for each (enables free fast-check arbitraries via `Arbitrary.make`)
48
+
49
+ ### 2. `card.ts` — Card primitives
50
+ - `Rank` (2-14), `Suit` ("c"|"d"|"h"|"s") as const objects + union types
51
+ - `Card` — readonly `{ rank, suit }` struct
52
+ - `ALL_CARDS` — 52-element constant array
53
+ - `toPokersolverString(card)` — converts to "Ad", "Th" format
54
+
55
+ ### 3. `deck.ts` — Deck with Effect-based shuffle
56
+ - `Deck = readonly Card[]`
57
+ - `shuffled: Effect<Deck>` — uses `Random.shuffle` (the **only** effectful operation in the engine)
58
+ - `draw(deck, count) => [drawn, remaining]` — pure
59
+ - `dealHoleCards(deck, seats) => [Map<SeatIndex, [Card,Card]>, remaining]` — pure
60
+ - `dealFlop / dealOne` — pure convenience functions
61
+
62
+ ### 4. `evaluator.ts` — pokersolver wrapper
63
+ - `HandRank` — our own type: `{ name, description, rank, bestCards }`
64
+ - `evaluate(cards) => HandRank` — wraps `pokersolver.Hand.solve`
65
+ - `compare(a, b) => -1|0|1`
66
+ - `winners(hands) => HandRank[]`
67
+ - `evaluateHoldem(holeCards, communityCards) => HandRank`
68
+ - Includes `declare module "pokersolver"` type declarations
69
+ - **pokersolver types never leak** — all conversion happens inside this module
70
+
71
+ ### 5. `player.ts` — Immutable player state
72
+ - `Player` — readonly struct: `{ seatIndex, chips, currentBet, isAllIn, isFolded, holeCards }`
73
+ - Pure transitions: `placeBet`, `fold`, `winChips`, `collectBet`, `dealCards`, `clearHand`
74
+ - Derived: `stack(player)` = chips - currentBet, `canAct(player)` = !folded && !allIn
75
+
76
+ ### 6. `action.ts` — Player actions + validation
77
+ - `Action` — discriminated union: Fold | Check | Call | Bet | Raise | AllIn
78
+ - `LegalActions` — what the active player can do: canFold, canCheck, callAmount, betRange, raiseRange
79
+ - `computeLegalActions(player, biggestBet, minRaise)` — pure computation
80
+ - `validateAction(action, legalActions) => Either<Error, Action>`
81
+
82
+ ### 7. `event.ts` — Game events (discriminated union)
83
+ - `GameEvent` — HandStarted, BlindsPosted, HoleCardsDealt, PlayerActed, BettingRoundEnded, CommunityCardsDealt, ShowdownStarted, PotAwarded, HandEnded, PlayerSatDown, PlayerStoodUp
84
+ - Events accumulated in state as a readonly array — no callbacks/event bus needed
85
+
86
+ ### 8. `error.ts` — Typed error hierarchy
87
+ - `InvalidAction`, `NotPlayersTurn`, `InvalidGameState`, `InsufficientChips`, `SeatOccupied`
88
+ - All extend `Data.TaggedError` for exhaustive pattern matching
89
+ - `PokerError` union type
90
+
91
+ ### 9. `pot.ts` — Side-pot calculation
92
+ - `Pot` — `{ amount: Chips, eligibleSeats: SeatIndex[] }`
93
+ - `collectBets(players, existingPots) => { pots, players }` — the min-bet collection algorithm
94
+ - `awardPots(pots, playerHands, buttonSeat) => awards[]` — distributes winnings, odd chips clockwise from button
95
+ - `totalPotSize(pots) => Chips`
96
+
97
+ ### 10. `betting.ts` — Betting round state machine
98
+ Combines poker-ts's `Round` + `BettingRound` into one module.
99
+ - `BettingRoundState` — name, players, activeIndex, activeSeatOrder, biggestBet, minRaise, lastAggressor, isComplete
100
+ - `createBettingRound(name, players, firstToAct, biggestBet, minRaise)`
101
+ - `applyAction(state, seat, action) => Either<Error, { state, events }>` — validates, applies, advances turn, detects completion
102
+ - `getLegalActions(state) => LegalActions`
103
+ - `activePlayer(state) => SeatIndex | null`
104
+
105
+ ### 11. `hand.ts` — Full hand lifecycle
106
+ The composition point. Manages phases: Preflop → Flop → Turn → River → Showdown → Complete.
107
+ - `HandState` — phase, players, communityCards, deck, pots, bettingRound, button, forcedBets, events
108
+ - `startHand(players, button, forcedBets) => Effect<HandState>` — effectful (shuffles deck)
109
+ - `act(state, seat, action) => Either<Error, HandState>` — **auto-advances** phase when betting round completes (deals community cards, starts next round, triggers showdown)
110
+ - `activePlayer`, `currentPhase`, `getLegalActions`, `getEvents` — read-only queries
111
+
112
+ ### 12. `table.ts` — Multi-hand session manager
113
+ - `TableState` — config, seats (Map), button, currentHand, handCount, events
114
+ - `createTable(config)`, `sitDown`, `standUp`
115
+ - `startNextHand(state) => Effect<TableState, PokerError>` — moves button, starts hand
116
+ - `act(state, seat, action) => Either<Error, TableState>` — forwards to hand module
117
+ - Button movement, busted player removal automatic
118
+
119
+ ---
120
+
121
+ ## Effect-TS Usage Strategy
122
+
123
+ | Where | What | Why |
124
+ |-------|------|-----|
125
+ | `deck.ts` shuffled | `Effect<Deck>` | Randomness is a side effect |
126
+ | `hand.ts` startHand | `Effect<HandState>` | Calls shuffle |
127
+ | `table.ts` startNextHand | `Effect<TableState, PokerError>` | Calls startHand |
128
+ | Everything else | Pure functions / `Either` | No side effects needed |
129
+ | Branded types | `Brand.refined` | Compile-time + runtime type safety |
130
+ | Errors | `Data.TaggedError` | Pattern matchable typed errors |
131
+ | Tests | `Random.make` with seed | Deterministic shuffle for reproducible tests |
132
+
133
+ We deliberately **avoid** `Layer`/`Context`/`Service` — this is a library, not an application.
134
+
135
+ ---
136
+
137
+ ## Testing Strategy
138
+
139
+ ### fast-check property tests (`test/properties/`)
140
+
141
+ **Pot invariants:**
142
+ - Chip conservation: sum of bets in === sum of pot amounts out
143
+ - Every non-folded player eligible for at least the main pot
144
+ - Side pots created only when bet levels differ (all-in scenarios)
145
+
146
+ **Betting round invariants:**
147
+ - Legal actions always non-empty for active player
148
+ - Player chips never go negative
149
+ - Total chip count constant throughout a round
150
+ - Betting round always terminates (no infinite loops)
151
+ - Calling sets bet equal to biggest bet; raising exceeds it
152
+
153
+ **Hand lifecycle invariants:**
154
+ - Phases progress strictly: Preflop → Flop → Turn → River → Showdown → Complete
155
+ - Community cards: 0 → 3 → 4 → 5
156
+ - Total chips (players + pots) constant throughout hand
157
+ - Hand always terminates
158
+
159
+ ### Schema-derived arbitraries
160
+ Use `Arbitrary.make(ChipsSchema)`, `Arbitrary.make(CardSchema)` etc. to generate valid domain values for property tests.
161
+
162
+ ### Unit tests per module (`test/*.test.ts`)
163
+ Each module gets focused unit tests for specific scenarios: known hand rankings, heads-up blinds, multi-way side pots, etc.
164
+
165
+ ---
166
+
167
+ ## Implementation Order (TDD — Tests First)
168
+
169
+ For each module: **write tests first**, then implement until tests pass. Build bottom-up.
170
+
171
+ | Step | Phase 1: Tests | Phase 2: Implementation |
172
+ |------|---------------|------------------------|
173
+ | 1 | Project setup | package.json, tsconfig, vitest config, install deps |
174
+ | 2 | `brand.test.ts` — Chips rejects negatives/floats, SeatIndex validates range | `brand.ts` + `error.ts` |
175
+ | 3 | `card.test.ts` — ALL_CARDS has 52 unique, toPokersolverString roundtrips | `card.ts` |
176
+ | 4 | `evaluator.test.ts` — known rankings (royal flush > full house), ties, kickers | `evaluator.ts` |
177
+ | 5 | `deck.test.ts` — shuffled has 52 unique cards, draw reduces size, empty deck errors | `deck.ts` |
178
+ | 6 | `player.test.ts` — bet reduces stack, fold sets flag, can't bet > chips, allIn detection | `player.ts` |
179
+ | 7 | `action.test.ts` — legal actions computed correctly for various scenarios | `action.ts` + `event.ts` |
180
+ | 8 | `pot.test.ts` + `pot.properties.ts` — side pots, chip conservation property | `pot.ts` |
181
+ | 9 | `betting.test.ts` + `betting.properties.ts` — turn order, round completion, chip invariants | `betting.ts` |
182
+ | 10 | `hand.test.ts` + `hand.properties.ts` — full hand scenarios, phase progression | `hand.ts` |
183
+ | 11 | `table.test.ts` — seating, button movement, multi-hand sessions | `table.ts` |
184
+ | 12 | `integration.test.ts` — end-to-end scenarios through public API | `index.ts` |
185
+
186
+ Each step: write failing tests → implement module → all tests green → move on.
187
+
188
+ ---
189
+
190
+ ## Verification
191
+
192
+ 1. **Unit tests**: `vitest run` — every module has focused tests
193
+ 2. **Property tests**: fast-check verifies invariants across thousands of random inputs
194
+ 3. **Integration test**: Full hand scenarios driven through `table.ts`:
195
+ - 2-player heads-up hand (fold preflop)
196
+ - 3-player hand to showdown with side pot
197
+ - Split pot scenario
198
+ - Multiple consecutive hands with button movement
199
+ - Player bust-out and removal
200
+ 4. **Type safety**: `tsc --noEmit` passes with strict mode — branded types catch misuse at compile time
201
+
202
+ ---
203
+
204
+ ## Project Setup
205
+
206
+ **Package manager:** pnpm
207
+
208
+ **Dependencies:**
209
+ - `effect` ^3.x — core library
210
+ - `pokersolver` ^2.1.4 — hand evaluation
211
+
212
+ **Dev dependencies:**
213
+ - `typescript` ^5.7, `vitest` ^3.x, `fast-check` ^3.x, `@effect/vitest`, `@fast-check/vitest`
214
+
215
+ **tsconfig:** strict, ES2022 target, NodeNext module, esModuleInterop (for pokersolver CJS)
package/README.md ADDED
@@ -0,0 +1,182 @@
1
+ # texasholdem
2
+
3
+ A modular, functional Texas Hold'em poker engine built with [Effect-TS](https://effect.website/).
4
+
5
+ Immutable state transitions, typed errors, branded domain types, and event logging — no mutation, no thrown strings, no callbacks.
6
+
7
+ ## Features
8
+
9
+ - **Immutable state machine** — `(state, action) => newState`, every transition returns a new state
10
+ - **Typed errors** — `Data.TaggedError` hierarchy for exhaustive pattern matching
11
+ - **Branded types** — `Chips`, `SeatIndex`, `HandId` prevent mixing up numbers at compile time
12
+ - **Event log** — every game action is recorded as a `GameEvent` for hand history / observability
13
+ - **Automatic phase advancement** — betting round completion triggers the next phase (deal, showdown) automatically
14
+ - **Side pots** — correct multi-way all-in pot splitting with odd-chip distribution
15
+ - **Configurable** — 2-10 seat tables, custom blinds/antes
16
+ - **Minimal Effect usage** — only deck shuffle is effectful; everything else is pure functions / `Either`
17
+ - **Hand evaluation** — delegated to [pokersolver](https://github.com/goldfire/pokersolver) behind a clean abstraction
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm install texasholdem
23
+ # or
24
+ pnpm add texasholdem
25
+ ```
26
+
27
+ **Peer dependency:** `effect` ^3.12
28
+
29
+ ## Quick Start
30
+
31
+ ```typescript
32
+ import { Effect, Either, Option } from "effect";
33
+ import {
34
+ Chips,
35
+ SeatIndex,
36
+ createTable,
37
+ sitDown,
38
+ startNextHand,
39
+ tableAct,
40
+ getActivePlayer,
41
+ getTableLegalActions,
42
+ Call,
43
+ Fold,
44
+ Check,
45
+ } from "texasholdem";
46
+
47
+ // 1. Create a table
48
+ const table = Either.getOrThrow(
49
+ createTable({
50
+ maxSeats: 6,
51
+ forcedBets: { smallBlind: Chips(5), bigBlind: Chips(10) },
52
+ })
53
+ );
54
+
55
+ // 2. Seat players
56
+ let state = Either.getOrThrow(sitDown(table, SeatIndex(0), Chips(1000)));
57
+ state = Either.getOrThrow(sitDown(state, SeatIndex(1), Chips(1000)));
58
+ state = Either.getOrThrow(sitDown(state, SeatIndex(2), Chips(1000)));
59
+
60
+ // 3. Start a hand (effectful — shuffles the deck)
61
+ state = Effect.runSync(startNextHand(state));
62
+
63
+ // 4. Game loop — act until the hand is complete
64
+ while (Option.isSome(getActivePlayer(state))) {
65
+ const seat = getActivePlayer(state).value;
66
+ const legal = Option.getOrThrow(getTableLegalActions(state));
67
+
68
+ // Pick an action based on legal moves
69
+ const action = legal.canCheck ? Check : Call;
70
+
71
+ state = Either.getOrThrow(tableAct(state, seat, action));
72
+ }
73
+
74
+ // 5. Hand is complete — check results
75
+ console.log(`Hand finished. Events: ${state.events.length}`);
76
+ ```
77
+
78
+ ## Architecture
79
+
80
+ 12 modules in strict bottom-up dependency order:
81
+
82
+ ```
83
+ brand.ts ─── card.ts ─── deck.ts ───────────────────┐
84
+ │ │ │
85
+ │ └── evaluator.ts (pokersolver wrap) │
86
+ │ │
87
+ ├── player.ts ── action.ts ── event.ts │
88
+ │ │ │ │
89
+ │ └── pot.ts ──┤ │
90
+ │ │ │ │
91
+ │ betting.ts ──┘ │
92
+ │ │ │
93
+ │ hand.ts ────────────────────────┘
94
+ │ │
95
+ └───────────────── table.ts
96
+
97
+ index.ts (barrel exports)
98
+ ```
99
+
100
+ ### Module Summary
101
+
102
+ | Module | Purpose |
103
+ |--------|---------|
104
+ | `brand` | Branded types: `Chips`, `SeatIndex`, `HandId` with runtime validation |
105
+ | `card` | `Card`, `Rank`, `Suit`, `ALL_CARDS`, pokersolver string conversion |
106
+ | `deck` | Shuffle (the only `Effect`), draw, deal hole cards / community cards |
107
+ | `evaluator` | Hand ranking via pokersolver — `evaluate`, `compare`, `winners` |
108
+ | `player` | Immutable player state + transitions: `placeBet`, `fold`, `winChips` |
109
+ | `action` | `Action` union (Fold/Check/Call/Bet/Raise/AllIn) + `LegalActions` computation |
110
+ | `event` | `GameEvent` discriminated union — full hand history in state |
111
+ | `error` | `PokerError` hierarchy via `Data.TaggedError` |
112
+ | `pot` | Side-pot calculation, pot merging, award distribution with odd-chip handling |
113
+ | `betting` | Betting round state machine: turn order, completion detection, action validation |
114
+ | `hand` | Full hand lifecycle: Preflop → Flop → Turn → River → Showdown → Complete |
115
+ | `table` | Multi-hand session: seating, button movement, busted player removal |
116
+
117
+ ## API Overview
118
+
119
+ ### Table-Level (multi-hand sessions)
120
+
121
+ ```typescript
122
+ createTable(config: TableConfig): Either<TableState, InvalidConfig>
123
+ sitDown(state, seat, chips): Either<TableState, SeatOccupied | TableFull>
124
+ standUp(state, seat): Either<TableState, SeatEmpty | HandInProgress>
125
+ startNextHand(state): Effect<TableState, PokerError>
126
+ tableAct(state, seat, action): Either<TableState, PokerError>
127
+ getActivePlayer(state): Option<SeatIndex>
128
+ getTableLegalActions(state): Option<LegalActions>
129
+ ```
130
+
131
+ ### Hand-Level (single hand)
132
+
133
+ ```typescript
134
+ startHand(players, button, forcedBets, handId): Effect<HandState, PokerError>
135
+ act(state, seat, action): Either<HandState, PokerError>
136
+ activePlayer(state): Option<SeatIndex>
137
+ getLegalActions(state): Option<LegalActions>
138
+ currentPhase(state): Phase
139
+ getEvents(state): readonly GameEvent[]
140
+ isComplete(state): boolean
141
+ ```
142
+
143
+ ### Actions
144
+
145
+ ```typescript
146
+ Fold // Give up the hand
147
+ Check // Pass (when no bet to match)
148
+ Call // Match the current bet
149
+ Bet({ amount }) // Open betting (branded Chips)
150
+ Raise({ amount }) // Raise over current bet (branded Chips)
151
+ AllIn // Put all remaining chips in
152
+ ```
153
+
154
+ ## Effect-TS Usage
155
+
156
+ | Where | What | Why |
157
+ |-------|------|-----|
158
+ | `deck.ts` shuffle | `Effect<Deck>` | Randomness is a side effect |
159
+ | `hand.ts` startHand | `Effect<HandState, PokerError>` | Calls shuffle |
160
+ | `table.ts` startNextHand | `Effect<TableState, PokerError>` | Calls startHand |
161
+ | Everything else | Pure functions / `Either` | No side effects needed |
162
+ | Branded types | `Brand.refined` | Compile-time + runtime safety |
163
+ | Errors | `Data.TaggedError` | Pattern-matchable typed errors |
164
+
165
+ No `Layer`/`Context`/`Service` — this is a library, not an application.
166
+
167
+ ## Testing
168
+
169
+ ```bash
170
+ pnpm test # run all tests
171
+ pnpm test:watch # watch mode
172
+ pnpm typecheck # tsc --noEmit
173
+ ```
174
+
175
+ 132 tests across 21 files:
176
+ - **Unit tests** — focused scenarios per module
177
+ - **Property-based tests** — fast-check verifies invariants (chip conservation, betting termination, phase progression) across thousands of random inputs
178
+ - **Integration tests** — end-to-end scenarios through the public API
179
+
180
+ ## License
181
+
182
+ MIT
@@ -0,0 +1,57 @@
1
+ // @ts-check
2
+ import eslint from "@eslint/js";
3
+ import tseslint from "typescript-eslint";
4
+ import effectPlugin from "@effect/eslint-plugin";
5
+ import prettierConfig from "eslint-config-prettier";
6
+
7
+ export default tseslint.config(
8
+ // Global ignores
9
+ {
10
+ ignores: ["dist/", "node_modules/", "**/*.d.ts", "vitest.config.ts"],
11
+ },
12
+
13
+ // Base ESLint recommended rules
14
+ eslint.configs.recommended,
15
+
16
+ // TypeScript recommended rules with type-checking
17
+ ...tseslint.configs.recommendedTypeChecked,
18
+ {
19
+ languageOptions: {
20
+ parserOptions: {
21
+ projectService: true,
22
+ tsconfigRootDir: import.meta.dirname,
23
+ },
24
+ },
25
+ },
26
+
27
+ // Effect plugin rules
28
+ {
29
+ plugins: {
30
+ "@effect": effectPlugin,
31
+ },
32
+ },
33
+
34
+ // Project-specific rule overrides
35
+ {
36
+ rules: {
37
+ // Allow type assertions used extensively for branded types (Chips, SeatIndex)
38
+ "@typescript-eslint/no-unsafe-argument": "off",
39
+ "@typescript-eslint/no-unsafe-assignment": "off",
40
+ "@typescript-eslint/no-unsafe-call": "off",
41
+ "@typescript-eslint/no-unsafe-member-access": "off",
42
+ "@typescript-eslint/no-unsafe-return": "off",
43
+ // Allow unused vars prefixed with _
44
+ "@typescript-eslint/no-unused-vars": [
45
+ "error",
46
+ { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
47
+ ],
48
+ // Allow empty object types in tagged errors (e.g., TableFull, HandInProgress)
49
+ "@typescript-eslint/no-empty-object-type": "off",
50
+ // Allow non-null assertions where array bounds are checked manually
51
+ "@typescript-eslint/no-non-null-assertion": "warn",
52
+ },
53
+ },
54
+
55
+ // Prettier — must be last to disable conflicting formatting rules
56
+ prettierConfig,
57
+ );
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "texasholdem",
3
+ "version": "1.0.0",
4
+ "description": "Modular, functional Texas Hold'em poker engine built with Effect-TS",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "test": "vitest run",
11
+ "test:watch": "vitest",
12
+ "typecheck": "tsc --noEmit",
13
+ "lint": "eslint src/ test/",
14
+ "lint:fix": "eslint --fix src/ test/",
15
+ "format": "prettier --write 'src/**/*.ts' 'test/**/*.ts'"
16
+ },
17
+ "dependencies": {
18
+ "effect": "^3.12.0",
19
+ "pokersolver": "^2.1.4"
20
+ },
21
+ "devDependencies": {
22
+ "@effect/eslint-plugin": "^0.3.2",
23
+ "@effect/vitest": "^0.27.0",
24
+ "@eslint/js": "^10.0.1",
25
+ "@fast-check/vitest": "^0.2.0",
26
+ "@typescript-eslint/eslint-plugin": "^8.56.0",
27
+ "@typescript-eslint/parser": "^8.56.0",
28
+ "eslint": "^10.0.0",
29
+ "eslint-config-prettier": "^10.1.8",
30
+ "fast-check": "^3.23.0",
31
+ "prettier": "^3.8.1",
32
+ "typescript": "^5.7.0",
33
+ "typescript-eslint": "^8.56.0",
34
+ "vitest": "^3.0.0"
35
+ }
36
+ }