Initial commit: Hold'em engine, bots, and simulation harness
Kotlin Multiplatform engine (JVM target only for now; androidTarget and iosArm64 slot in without touching commonMain). Core: - HandEvaluator: single-pass 5-7 card evaluation, ~24M evals/sec. Verified exhaustively against published frequencies for all 2,598,960 five-card hands. - Equity: Monte Carlo with ties split. PreflopChart ranks the 169 starting hands using all-in equity plus an explicit playability adjustment, so looseness means "plays the top N%". - Table: no-limit betting rounds, side pots, odd-chip splits, uncalled-bet refunds, and incomplete (short all-in) raises that correctly do not reopen betting. Bots: - SkillLevel and PlayStyle are orthogonal axes. Skill drives decision quality (rollout accuracy, pot-odds discipline, position awareness, error rate); style drives bluffing, sandbagging, aggression, tightness. - BotMood gives tilt that persists between hands and decays. - OpponentModel lets Advanced/Expert exploit habitual bettors. - MathBot emits a DecisionTrace of the numbers behind each decision, which the coach will later hand to an LLM to narrate. The LLM never does poker maths. Simulator: - 2,200-3,400 hands/sec. Deck RNG is separate from bot RNGs so rollout counts cannot shift the deal. - Controlled skill-ladder test asserts the difficulty gradient is monotonic: 73.9 / 53.9 / 27.6 / -155.4 bb/100 over 50k hands. Assets: 52 CC0 English-pattern card faces plus generated backs. Tests: 30 passing (evaluator, table rules, pre-flop chart). Known open: win-rate magnitudes ~10x realistic and several profiles looser than their labels. Tuning, not correctness. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
# Poker — Texas Hold'em with teachable AI opponents
|
||||
|
||||
Kotlin Multiplatform. Ships iOS + Android; Android first (only Android hardware
|
||||
for physical testing).
|
||||
|
||||
## Build
|
||||
|
||||
No `java`/`gradle` on PATH — use Android Studio's bundled JDK:
|
||||
|
||||
```bash
|
||||
export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
|
||||
./gradlew :engine:jvmTest # evaluator + engine tests
|
||||
./gradlew :sim:run --args="50000" # simulate 50k hands, print bot stats
|
||||
```
|
||||
|
||||
## Layout
|
||||
|
||||
| Path | What |
|
||||
|---|---|
|
||||
| `engine/src/commonMain/.../core/` | Cards, evaluator, equity, pre-flop chart |
|
||||
| `engine/src/commonMain/.../bot/` | Skill/style profiles, `MathBot` |
|
||||
| `engine/src/commonMain/.../game/` | `Table` — betting rounds, side pots, showdown |
|
||||
| `sim/` | JVM-only headless simulator used to **tune** bot profiles |
|
||||
| `assets/cards/` | 52 CC0 card faces + generated backs |
|
||||
|
||||
`engine` is pure Kotlin with no platform APIs, so `androidTarget()` /
|
||||
`iosArm64()` slot in without touching `commonMain`.
|
||||
|
||||
## Design rules
|
||||
|
||||
1. **Poker maths never goes near the LLM.** Difficulty and style are engine-side
|
||||
EV/frequency calculations — instant, deterministic, testable, offline. The LLM
|
||||
only narrates numbers the engine already computed (`DecisionTrace`), and adds
|
||||
persona/table talk.
|
||||
2. **Skill and style are orthogonal.** `SkillLevel` = how correct decisions are;
|
||||
`PlayStyle` = bluffing, sandbagging, aggression, tightness. Build the strongest
|
||||
bot, then inject *controlled error* for lower tiers.
|
||||
3. **Pre-flop is range-based, not equity-based.** All-in equity overvalues trash
|
||||
(7-2o has ~35% vs one random hand but is unplayable). `PreflopChart` ranks the
|
||||
169 starting hands so `looseness` means "plays the top N%".
|
||||
4. **The simulator is how bots get tuned.** Run it after any bot change; it prints
|
||||
a controlled skill-ladder test that must stay monotonic.
|
||||
|
||||
## Testing notes
|
||||
|
||||
- `Table` takes a `CardSource`, so `StackedDeck.of(holes, board)` gives fully
|
||||
deterministic hands. Use it for any rule test.
|
||||
- The simulator gives the **deck its own RNG**, separate from each bot's. Never
|
||||
share one: bots consume RNG proportional to their `equityIterations`, so a
|
||||
shared stream means changing a profile silently changes the cards dealt.
|
||||
- `./gradlew :sim:run --args="chart"` dumps the starting-hand ranking.
|
||||
- Small samples lie. 1,000 hands is not enough to rank profiles — use 50,000+
|
||||
before believing a gradient.
|
||||
|
||||
## Status
|
||||
|
||||
- Evaluator: verified exhaustively against published frequencies for all
|
||||
2,598,960 five-card hands. ~24M evals/sec.
|
||||
- Engine: chip-conserving; side pots, odd-chip splits, uncalled-bet refunds, and
|
||||
incomplete (short all-in) raises all covered by tests.
|
||||
- Bots: skill gradient **passes** monotonically (73.9 / 53.9 / 27.6 / −155.4
|
||||
bb/100 at 50k hands).
|
||||
- Known-imperfect: win-rate magnitudes are still ~10x realistic, and several
|
||||
profiles are looser than their labels (the Rock plays ~38% VPIP, should be
|
||||
~12%). Tuning is the open work.
|
||||
- Gradle emits an `archives` deprecation from the Kotlin Multiplatform plugin's
|
||||
own `jvm()` target registration — upstream in Kotlin 2.2.10, not our build.
|
||||
- Not built yet: LLM persona layer, opt-in coach, Compose UI.
|
||||
Reference in New Issue
Block a user