pkcore 0.0.39

Prototype core poker library.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
# Poker Platform Roadmap

A technical demonstration platform built on `pkcore` that showcases AI
decision-making, OpenTelemetry observability, and distributed systems
design — all through the lens of a live poker table.

## Vision

A running poker table service where:
- **Remote clients** (AI agents, human players) connect via gRPC and play
  hands
- **A web spectator app** shows all hole cards and live table action in
  real time (PokerGo-style)
- **AI agents** powered by different stacks (Claude, OpenAI, local LLMs,
  rule-based) compete against each other
- **OTel instrumentation** exposes traces, metrics, and logs for every
  game event, making the platform a live demo of observability patterns

---

## Current State

| Repo | Status | Notes |
|------|--------|-------|
| [pkcore]https://github.com/folkengine/pkcore | Active | Full poker library: `Table`, `Dealer`, `Player`, `Game`, card evaluation, GTO analysis |
| [pkdealer]https://github.com/ImperialBower/pkdealer | Skeleton | gRPC proto fully defined; only `Ping` is implemented; workspace has `proto`, `service`, `client` crates |
| [pkbot]https://github.com/ImperialBower/pkbot | Skeleton | Bot personality library; YAML-serializable range and betting strategies for use by pkdealer agent clients |
| [pkgto-web]https://github.com/ImperialBower/pkgto-web | Active | WASM preflop equity analyzer; single `analyze_gto` function, deployed to GitHub Pages |

The `dealer.proto` in pkdealer already defines all the RPCs needed:
`SeatPlayer`, `StartHand`, `Act`, `AdvanceStreet`, `EndHand`,
`StreamEvents`, `GetStatus`, etc. The foundation is solid — it just needs
to be wired up.

---

## Architecture Overview

```
┌─────────────────────────────────────────────────────────────────┐
│                        pkdealer workspace                        │
│                                                                  │
│  ┌─────────────────┐   ┌──────────────────┐   ┌─────────────┐  │
│  │  pkdealer_proto │   │ pkdealer_service  │   │ pkdealer_   │  │
│  │  (generated     │   │ (gRPC server +   │   │ spectator   │  │
│  │   gRPC types)   │   │  game engine)    │   │ (web app)   │  │
│  └─────────────────┘   └──────────────────┘   └─────────────┘  │
│           │                     │                     │          │
│           └─────────────────────┼─────────────────────┘          │
│                                 │ pkcore                          │
└─────────────────────────────────┼─────────────────────────────────┘
         ┌────────────────────────┼────────────────────────┐
         │                        │                         │
         ▼                        ▼                         ▼
  ┌─────────────┐         ┌──────────────┐         ┌──────────────┐
  │  AI Agent   │         │  AI Agent    │         │  AI Agent    │
  │  (Claude)   │         │  (OpenAI)    │         │  (rule-based)│
  └─────────────┘         └──────────────┘         └──────────────┘
         │                        │                         │
         └────────────────────────┼─────────────────────────┘
                           gRPC (port 50051)

  Browser ──── WebSocket/SSE ──── pkdealer_spectator ──── gRPC ──── pkdealer_service
```

### Component Responsibilities

**`pkdealer_service`** — The table authority
- Owns the `Table` state (from pkcore)
- Exposes the full gRPC `DealerService`
- Manages a `tokio::sync::broadcast` channel for table events
- Drives street progression (or exposes hooks for an orchestrator to do
  it)
- Emits OTel spans and metrics for every action

**`pkdealer_spectator`** — Web broadcast app (new crate in pkdealer
workspace)
- Axum web server
- Subscribes to the event stream from the dealer service
- Serves an SSE endpoint to browsers
- Renders a table view where **all hole cards are visible**
  (broadcast/spectator mode)
- Shows pot, board, action log, chip counts in real time

**`pkbot`** — Bot personality library ([ImperialBower/pkbot](https://github.com/ImperialBower/pkbot))
- Standalone Rust library (not part of the pkdealer workspace)
- Defines `BotProfile` — a fully serializable bot personality combining a
  GTO range strategy and a betting strategy
- Profiles are stored as YAML and loaded via `serde` + `serde_yaml`
- Covers preflop range charts, postflop betting tendencies, aggression
  factors, and bluff frequencies
- Different profiles produce different player archetypes: tight-passive,
  loose-aggressive, GTO-solver-driven, etc.
- Agent binaries in pkdealer load a `BotProfile` from a YAML file at
  startup and use it to drive decisions via the gRPC `Act` RPC

**AI Agent clients** — Separate binaries/crates
- Each implements the same gRPC client interface
- Each loads a `BotProfile` from pkbot to drive decision-making
- Connects as a player, receives its own hole cards and table state, acts
  via gRPC

---

## pkcore Epics

| Epic | Topic | Status |
|------|-------|--------|
| [EPIC-14]docs/EPIC-14_Equity.md | Hand Equity — pot odds, EV, range equity, weighted ranges | Complete |
| [EPIC-15]docs/EPIC-15_GTO_Solver.md | GTO Solver — game tree, CFR, strategy profiles, exploitability | Complete |
| [EPIC-16]docs/EPIC-16_DCFR.md | CFR+ and Discounted CFR — faster convergence variants | Complete |
| [EPIC-17]docs/EPIC-17_Kuhn_Poker.md | Kuhn Poker — minimal 3-card game, analytical Nash, CFR validator, interactive examples | Complete |
| EPIC-18 | Range Frequencies — optional per-combo frequency in range strings (`AA:0.5`) | Planned |

---

## pkgto-web Updates

[pkgto-web](https://github.com/ImperialBower/pkgto-web) is a WASM-powered
preflop equity analyzer that runs entirely in the browser via a single
`analyze_gto(hero, villain_range)` function compiled from pkcore.

### Planned UI updates

#### Range frequency display (depends on EPIC-18)

Once pkcore supports per-combo frequencies in range strings, the UI should
surface them:

- **Range input** — accept the `:f` suffix in the villain range text field
  (e.g. `AA:0.5, KK, QQ:0.75`); show a validation error if the value is
  outside `[0.0, 1.0]`
- **Matchup table** — add a `Frequency` column showing each combo's weight;
  grey out or visually de-emphasise combos below a configurable threshold
- **Combined odds** — weight the combined equity calculation by combo
  frequency so the output reflects the actual mixed-strategy distribution
  rather than assuming every combo is played 100%
- **Range display** — render the villain range summary with frequency
  annotations so users can confirm what was parsed

#### `analyze_gto` WASM API extension

The Rust side needs a corresponding update:

- `GtoResult` gains `frequency: f32` on each `MatchupEntry`
- Combined odds already use `WeightedCombos` internally; ensure combo-level
  frequency is threaded through from the parsed range string
- Return the normalised range string (with frequencies) in `GtoResult` so
  the JS layer can display exactly what pkcore parsed

#### Stretch: range builder UI

A click-to-build range interface where each hand can be toggled between
`0%`, `25%`, `50%`, `75%`, and `100%` frequency — outputting a
frequency-annotated range string that feeds into `analyze_gto`.

---

## Implementation Phases

### Phase 1 — Complete the pkdealer gRPC Server

**Goal:** A fully functional gRPC poker table server.

**Work:**
1. Implement all `DealerService` methods in `pkdealer_service` using
   `pkcore::Table` and `pkcore::Dealer`
2. Implement `StreamEvents` using a
   `tokio::sync::broadcast::Sender<TableEvent>` shared across connections
3. Add a game loop binary (`pkdealer_orchestrator`) that drives hand
   progression:
   - Seat players → start hand → prompt each player to act → advance
     streets → end hand → repeat
4. Wire hole card visibility: the server knows all cards; `GetStatus`
   returns hole cards only for the requesting player's seat; a separate
   admin/spectator token reveals all cards

**Key decisions:**
- Use `Arc<Mutex<Table>>` for shared mutable table state across gRPC
  handlers
- Game phase enforcement: RPCs return `PermissionDenied` if called out of
  order
- Reconnect support: players identify by a UUID issued at `SeatPlayer`
  time

**Deliverable:** `cargo run --bin pkdealer_service` starts a server; the
existing `pkdealer_client` can ping, seat a player, start a hand, act,
and get status.

---

### Phase 2 — Web Spectator App

**Goal:** A browser tab that looks like watching poker on PokerGo —
everyone's cards visible, live updates.

**Work:**
1. Add `pkdealer_spectator` crate to the pkdealer workspace
2. Axum routes:
   - `GET /` — serve the table UI (HTML + minimal JS)
   - `GET /events` — SSE stream of table events
   - `GET /state` — current full table snapshot (JSON, all cards visible)
3. The spectator crate connects to `pkdealer_service` via the gRPC
   `StreamEvents` RPC using a spectator auth token
4. Frontend (HTMX + Tailwind or plain HTML/CSS/JS):
   - Playing card rendering (SVG or CSS card components)
   - Seat positions around an oval table
   - Dealer button, blinds, pot display
   - Action log sidebar
   - Animated card dealing and chip movement

**Suggested tech for the frontend:**
- React or Vue (TBD) for a polished, interactive UI with animations
- [Tailwind CSS]https://tailwindcss.com/ for layout and styling
- SVG playing card assets (e.g.,
  [cardstarter]https://github.com/htdebeer/SVG-cards)
- Card deal animations, chip movement, and action highlights to match a
  broadcast-quality feel

**Deliverable:** Open `http://localhost:3000` and watch a live game with
all cards face-up.

---

### Phase 3 — OpenTelemetry Instrumentation

**Goal:** Make every game event observable. This phase is the core
"technical demonstration" value.

**Work:**
1. Add `opentelemetry`, `opentelemetry-otlp`, `tracing-opentelemetry`,
   `tracing` crates to `pkdealer_service`
2. Instrument with spans:
   - `hand` span — covers deal through showdown; attributes: hand_id,
     player_count, starting_pot
   - `action` span — child of `hand`; attributes: seat, action_type,
     amount, pot_after
   - `street` span — child of `hand`; attributes: street_name,
     board_cards
3. Add metrics:
   - `pkdealer.hands_played` counter
   - `pkdealer.pot_size` histogram
   - `pkdealer.action_duration_ms` histogram (time from prompt to act)
   - `pkdealer.ai_decision_latency_ms` histogram (tag by agent type)
4. Propagate trace context into gRPC metadata so client spans nest under
   server spans
5. Add `docker-compose.yml` with:
   - Jaeger (or Grafana Tempo) for traces
   - Prometheus for metrics
   - Grafana for dashboards

**Deliverable:** `docker compose up` + run a game → open Jaeger at
`http://localhost:16686` and see a full hand trace with action-level
spans. Open Grafana and see a live game stats dashboard.

---

### Phase 4 — Bot Personalities & AI Agent Clients

**Goal:** Multiple AI personalities playing at the same table, each with
observable decision-making. Bot profiles are defined in pkbot and loaded
by agent binaries in pkdealer.

**pkcore work (prerequisite for pkbot):**

Extend the range string format to support optional per-combo frequencies
using a colon followed by a value in `[0.0, 1.0]`:

```
AA:0.5, KK, QQ:0.75, AKs:1.0
```

A combo with no frequency suffix defaults to `1.0` (played 100% of the
time). This affects how range charts are stored, displayed, and consumed
by the solver and bot logic.

- Extend `Combo` to carry an optional `frequency: Option<f64>` field
- Update the range string parser (`FromStr` for `Combos`) to recognise the
  `:f` suffix and validate that the value is in `[0.0, 1.0]`
- Update `Display` for `Combo` to emit the suffix when frequency is not
  `1.0` (omit it otherwise to keep round-tripped strings clean)
- Ensure `WeightedCombos` respects combo-level frequency when expanding
  ranges — a combo at frequency `0.5` contributes weight `0.5` per hand
- Add a pkcore EPIC doc (`docs/EPIC-18_Range_Frequencies.md`) covering
  the full design and test plan

**pkbot work (prerequisite for rule-based and GTO agents):**
1. Define `BotProfile` struct with preflop range charts (using frequency-
   annotated range strings), postflop betting tendencies, aggression
   factor, and bluff frequency
2. Add `serde` + `serde_yaml` — serialize/deserialize profiles to YAML
3. Ship a set of named reference profiles: `tight_passive.yaml`,
   `loose_aggressive.yaml`, `gto_river.yaml`
4. Publish to crates.io so pkdealer agent binaries can depend on it

**Approach:** Define a shared `PokerAgent` trait (or just a convention)
that each agent implements:

```rust
trait PokerAgent {
    async fn decide(&self, hand_state: &HandState) -> PlayerAction;
}
```

`HandState` is derived from `GetStatus` + the agent's own hole cards.

**Agents to build (in order of complexity):**

#### 4a. Random Agent (baseline)
- Picks a legal action at random
- Establishes the benchmark and proves the plumbing works

#### 4b. Rule-Based Agent
- Uses pkcore's `Eval`, `Outs`, and `TheNuts` to assess hand strength
- Simple heuristics: fold weak hands preflop, bet strong hands,
  check/call marginal hands
- No AI API required — demonstrates pkcore's analysis capabilities

#### 4c. Claude Agent (Anthropic)
- Uses the Anthropic Rust SDK or HTTP API
- Sends a natural-language prompt describing the hand state
- Parses the LLM response into a `PlayerAction`
- Prompt includes: hole cards, board, pot odds, position, stack sizes,
  action history
- Emits OTel spans using `gen_ai.*` semantic conventions (see Langfuse
  section below)

#### 4d. OpenAI Agent
- Same pattern as Claude agent, using the OpenAI API
- Enables direct A/B comparison of model decision-making via OTel
  dashboards and Langfuse

#### 4e. Local LLM Agent (stretch)
- Uses [Ollama]https://ollama.com/ with a local model (Llama 3,
  Mistral, etc.)
- Same prompt format as cloud agents
- Demonstrates offline/on-premises AI

**Each agent:**
- Lives in its own binary (e.g., `pkdealer_agent_claude`,
  `pkdealer_agent_random`)
- Emits OTel traces including the decision prompt and response
- Can be started with a seat number and player name argument

#### 4f. LLM Observability with Langfuse

[Langfuse](https://langfuse.com/) is an open-source LLM observability
platform that complements OTel for the AI-specific layer. While
OTel/Jaeger/Grafana cover game mechanics (hand spans, action latency, pot
metrics), Langfuse covers the LLM interaction layer:

- **Full prompt/completion capture** — browse every poker decision with
  the exact prompt sent and response received
- **Token usage and cost tracking** — per decision, per model, per
  session; easily compare Claude vs OpenAI spend
- **Prompt versioning** — iterate on the poker system prompt and track
  how win rates change across versions
- **Scoring** — feed hand outcome (won/lost/folded correctly) back as a
  numeric score on each LLM trace, building a labeled dataset of good vs.
  bad AI decisions over time
- **Side-by-side model comparison** — Langfuse's UI makes it easy to
  spot behavioral differences (e.g., "Claude folds rivers more than
  GPT-4o")

**Integration approach (Rust-friendly):** There is no official Rust SDK,
but Langfuse supports an OpenTelemetry-native ingestion mode. Each LLM
agent emits spans using the
[OpenTelemetry Semantic Conventions for Generative AI](https://opentelemetry.io/docs/specs/semconv/gen-ai/)
(`gen_ai.*` attributes). Langfuse ingests these spans via its OTLP
endpoint — no vendor SDK required, keeping agent code clean and portable.

Key `gen_ai.*` attributes to emit per decision span:
- `gen_ai.system``"anthropic"`, `"openai"`, `"ollama"`
- `gen_ai.request.model` — e.g. `"claude-sonnet-4-6"`
- `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`
- `gen_ai.request.max_tokens`
- Custom: `poker.hand_id`, `poker.street`, `poker.pot_odds`,
  `poker.action_chosen`

**Scoring workflow:** After each hand resolves, the orchestrator calls the
Langfuse HTTP API to post a score (e.g., `+1` for winning the pot, `-1`
for a -EV fold) against the trace IDs for that hand's LLM decisions. Over
many hands this produces a leaderboard of model effectiveness.

**Demo value:** OTel/Jaeger shows the game timeline; Langfuse shows the
AI reasoning. Two browser tabs open during a live demo tell the complete
story — infrastructure observability and LLM observability side by side.

---

### Phase 5 — Demo Scenarios & Packaging

**Goal:** Make this easy to run as a live demo at a conference or in a
blog post.

**Work:**
1. `docker-compose.yml` that starts the full stack:
   - `pkdealer_service`
   - `pkdealer_spectator`
   - 4–6 AI agent containers (mix of models)
   - Jaeger
   - Prometheus + Grafana
   - Langfuse (self-hosted)
2. A `demo.sh` script that:
   - Starts the stack
   - Waits for services to be healthy
   - Seats all agents and starts the first hand
   - Opens the spectator URL in the browser
3. Grafana dashboard JSON (committed to repo) showing:
   - Active hand timeline
   - Per-agent win rate
   - Per-agent decision latency (Claude vs OpenAI vs local)
   - Pot size distribution
4. Langfuse dashboards:
   - Prompt version leaderboard (win rate by prompt version)
   - Per-model cost per hand
   - Decision quality scores over time
4. A `DEMO.md` walkthrough for presenting this live

---

## Technology Stack Summary

| Layer | Technology | Rationale |
|-------|-----------|-----------|
| Game engine | pkcore (Rust) | Already exists, battle-tested |
| Bot personalities | pkbot (Rust) | YAML-serializable profiles; decoupled from agent transport layer |
| RPC | gRPC / Tonic | Proto already defined in pkdealer; type-safe, streaming |
| Web server | Axum | Idiomatic async Rust, SSE support |
| Frontend | React or Vue + Tailwind | Polished UI with animations; framework TBD |
| Observability | OpenTelemetry (OTLP) | Vendor-neutral; works with Jaeger, Grafana, Honeycomb |
| LLM Observability | Langfuse | Prompt/completion capture, cost tracking, scoring, model comparison |
| AI (cloud) | Anthropic SDK, OpenAI SDK | Demonstrates different model behaviors |
| AI (local) | Ollama HTTP API | Offline demo capability |
| Infra | Docker Compose | Single-command demo startup |

---

## Repo Structure (end state)

```
pkbot/                         # Bot personality library (standalone crate)
├── src/
│   ├── lib.rs
│   ├── profile.rs             # BotProfile — top-level serializable type
│   ├── range_strategy.rs      # Preflop range charts + postflop tendencies
│   └── betting_strategy.rs    # Aggression factor, bluff freq, sizing rules
├── profiles/
│   ├── tight_passive.yaml
│   ├── loose_aggressive.yaml
│   └── gto_river.yaml
└── Cargo.toml

pkdealer/
├── crates/
│   ├── pkdealer_proto/        # Protobuf types (existing)
│   ├── pkdealer_service/      # gRPC server + game engine (expand)
│   ├── pkdealer_spectator/    # Axum web app + SSE (new)
│   ├── pkdealer_agent_random/ # Random baseline agent (new)
│   ├── pkdealer_agent_rules/  # Rule-based agent using pkbot profiles (new)
│   ├── pkdealer_agent_claude/ # Claude AI agent (new)
│   ├── pkdealer_agent_openai/ # OpenAI agent (new)
│   └── pkdealer_client_human/ # Interactive TUI client for human players (new)
├── docker-compose.yml         # Full demo stack (new)
├── demo.sh                    # One-command demo launcher (new)
├── grafana/
│   └── dashboards/            # Pre-built Grafana dashboards (new)
└── DEMO.md                    # Presenter guide (new)
```

---

## Open Questions / Decisions Needed

1. **Game flow ownership**: ✅ `pkdealer_service` drives the game loop
   autonomously — streets auto-advance once all players have acted, and a
   new hand starts automatically after showdown. The
   `pkdealer_orchestrator` crate is not needed and can be removed from
   scope.

2. **Human players**: ✅ Supported via a dedicated
   `pkdealer_client_human` binary — a terminal UI (TUI) client that
   connects via gRPC like any AI agent. The spectator web UI remains
   read-only. This keeps the spectator simple and auth-free while still
   allowing a human to sit at the table; the TUI shows only that player's
   hole cards and prompts for actions at their turn.

3. **Single table vs. multi-table**: ✅ Single table for now. Design the
   service with multi-table expansion in mind (pkcore's `TableManager`
   already supports it), but do not implement it yet. Multi-table support
   is a future phase.

4. **Frontend complexity**: ✅ React or Vue with a polished,
   production-quality look. Card animations, smooth chip transitions, and
   a visually impressive table are worth the added complexity for demo
   impact. Framework choice (React vs Vue) TBD, but either pairs well
   with the Axum SSE backend via a WebSocket or EventSource connection.

5. **pkcore dependency in pkdealer**: ✅ pkcore is already published to
   crates.io — the GitHub repo is private but the crate is public.
   pkdealer can depend on it via crates.io as normal; no path dependency
   or GitHub source needed.

6. **Auth model**: ✅ Use a simple shared secret token (gRPC metadata for
   player clients, query param or header for the spectator SSE endpoint)
   for the POC. Design auth as a pluggable layer from the start so it can
   be replaced with a real system (e.g., JWT + OAuth2) without
   restructuring the service. No auth refactoring should be required to
   add it later.