@jurojinpoker/pps-handsummaryjs
v1.0.6
Published
Browser-friendly single-hand JSON summary + legacy hands.txt helpers and local CLI
Readme
PPS.HandSummaryJS
Browser-friendly library (TypeScript → ESM in dist/) for working with ProcessedHandsDump JSON — the same hands.txt files produced by PPS.SandBox.Console. Use it from a hand replayer or any bundler (Vite, webpack, etc.): fetch the JSON, then call parseProcessedHandsJson, getHandAtOneBasedIndex, and helpers under src/poker/.
A small CLI (src/cli.ts → dist/cli.js) exists only to run local tests against real hands.txt files (filesystem + console.log). It is not required in production web.
Library vs CLI
| Artifact | Purpose |
|----------|---------|
| dist/index.js + dist/index.d.ts | Public API for the web app and any consumer. No Node-only imports; safe to bundle. |
| dist/cli.js | Node-only test harness: read file from disk, print one hand. Run after npm run build. |
Add new poker logic in src/poker/ (new modules under src/poker/, re-export from src/poker/index.ts and src/index.ts as needed).
Web / bundler import
After npm run build, point your app at the package entry (or a relative path to dist):
import {
parseProcessedHandsJson,
getHandAtOneBasedIndex,
type ProcessedHandsDump,
type HandSummary,
} from 'pps.handsummaryjs';
// or: from '../PPS.HandSummaryJS/dist/index.js'
async function loadDump(url: string): Promise<ProcessedHandsDump> {
const text = await fetch(url).then((r) => r.text());
return parseProcessedHandsJson(text, url);
}The JSON shape matches what the .NET console writes; no adapter layer is required if you use the same files.
Input format: ProcessedHandsDump
See WriteProcessedHandsJson in PPS.SandBox.Console/Program.cs.
| Property | Meaning |
|----------|---------|
| Run | Run metadata (optional for parsing a single hand). |
| Hands | HandSummary array (successful parses). Required. |
Typical path: PPS.SandBox.Console/processedJsons/run-yyyyMMdd-HHmmss/hands.txt
With --parse-only + --save-processed-json, only successful parses appear in Hands.
Public API (summary)
| Export | Role |
|--------|------|
| Types | ProcessedHandsDump, HandSummary / HandSummaryPartial, ProcessedHandsRunMetadata |
| parseProcessedHandsJson | Parse JSON string → validated dump (throws ProcessedHandsParseError) |
| assertProcessedHandsDump | Validate an already-JSON.parsed value |
| isProcessedHandsDump | Type guard |
| getHandAtOneBasedIndex | Select one hand by 1-based index (throws HandIndexError) |
| buildHandDisplayLines | Debug text lines (optional for UI) |
| countStreetActions | Per-street action counts |
| buildHandActionAnalytics | Per-action JSON: pot, potOdds, spr, effectiveStack, mdf, milestones (see below) |
| buildHandBreadcrumbFromAnalytics, buildHandBreadcrumbSummary | Breadcrumb model: byStreet, timeline, streetBreadcrumbStrings, playersInBreadcrumb — milestone labels like PlayerActionPlay; folds omitted on early streets; folds on the last street with action (often river) kept (who folded to a bet/raise). Other actions unchanged. buildHandSummaryFromJson also sets breadcrumb. |
| PlayerActionMilestoneType, classifyPlayerActionMilestone | Same enum names as Jurojin PlayerActionMilestoneType / PlayerActionPlaysCreator (src/poker/actionMilestone.ts) |
Action analytics (per street / per action)
buildHandActionAnalytics(hand) returns HandActionAnalyticsJson: a flat actions array plus streets grouped the same way. Each row is one PlayerAction in order, with:
| Field | Meaning |
|-------|--------|
| potBefore / potAfter | Main pot before/after that action (antes, blinds, then Amount added to pot). |
| toCall | Chips needed to match the current face bet before this player acts. |
| potOdds | If toCall > 0: PotOddsDetail — effectiveCall = min(toCall, acting player’s stack) so short all-ins use lower risk; riskReward = potBefore : effectiveCall (each side ≤ one decimal); percentage / requiredEquity from effectiveCall / (potBefore + effectiveCall). Otherwise null. |
| spr | Stack-to-pot ratio: effectiveStack / potBefore when both > 0. |
| effectiveStack | With hero identified and still in the hand: min(hero stack, max opponent stack) among players still in (max the hero can lose at this point). Otherwise: minimum remaining stack among all still in. |
| mdf | Only when heroIdentified and this row is hero’s action with toCall > 0: MDF = (potBefore − toCall) / potBefore (equivalent to P/(P+B) where potBefore is the pot including the bet to call). Otherwise null. |
| milestoneType / milestoneAmount / rawActionType | Semantic play (Limp, OpenRaise, CBet, DonkBet, ThreeBet, …) and amount mirror Jurojin PlayerActionPlay; logic is evaluated per action with street context (not only the last action on the street). UI strings: PLAYER_ACTION_MILESTONE_DESCRIPTIONS. |
Simulation uses Amount / AbsoluteAmount from the PokerStars-style parser (see HandHistoryParser in PPS.Core.PokerStars). Multi-way pots use the same formulas as a simplified heads-up model; treat as approximate for complex spots.
ParsingService on your machine (raw HH → API → HandSummaryJS)
Use this when PPS.ParsingService is running locally (default http://localhost:5111 — see Properties/launchSettings.json).
npm run build- Start the API, e.g. from repo root:
dotnet run --project PPS.ParsingService/PPS.ParsingService.csproj - Point the script at a single-hand raw text file (PokerStars-style HH, same content you would paste into
HandContent):
npm run consume-local-api-hand -- path/to/one-hand.txt| Flag | Meaning |
|------|--------|
| --url <base> | API base URL (default: PARSING_SERVICE_URL env or http://localhost:5111) |
| --site <n> | Optional PokerSite enum value (e.g. 1 = PokerStars) |
| --out <dir> | Output folder (default: .parsing-api-smoke/, gitignored) |
The script POSTs /api/hh/parse-hand, validates the wire JSON (assertWireParseHandResponse), maps camelCase → PascalCase (wireKeysToHandSummaryPartial), runs buildHandReplaySummary, and writes 01-response.json, 02-wire-validation.txt, 03-hand-replay-summary.json (or skip/error files if the parse failed).
Alias: npm run parsing-api-smoke — same script.
If you see Fetch failed, the script could not reach the API (usually ParsingService not running or wrong --url). Start the service first, then check http://localhost:5111/api/hh/health in a browser. On Windows PowerShell, $ in the file path must be quoted so it is not expanded — use single-quoted path or backtick-escape $ (see 01-fetch-error.txt for the underlying errno after npm run build).
CLI reference (local testing only)
Run from repo root or PPS.HandSummaryJS (run npm run build first).
| Argument | Required | Description |
|----------|----------|-------------|
| --hand-index <n> | Yes (unless --help) | 1-based index into Hands. |
| --file <path> | No | Path to hands.txt. If set, --dir is not used for resolution. |
| --dir <path> | No | Base dir with run-* folders. Default: PPS.SandBox.Console/processedJsons. Latest run-* wins. |
| --help, -h | No | Usage. |
| --json | No | Print buildHandActionAnalytics output as JSON (includes sourceFile, handIndexOneBased, totalHandsInFile). |
Resolving the file when --file is omitted
- Resolve
--diragainstprocess.cwd()if relative. - List
run-*directories, sort descending by name. - Read
hands.txtinside the first folder.
Output (CLI)
- Default: plain text on stdout, ending with an OK line and
Source file:path. --json: structured JSON (one hand’s analytics + metadata).
Errors: stderr, exit code 1.
Project layout
| Path | Role |
|------|------|
| src/index.ts | Library entry: re-exports types + src/poker/. |
| src/types.ts | JSON / HandSummary shapes (extend as the replayer needs). |
| src/poker/ | Poker logic: parsing, display helpers; add new files here. |
| src/cli.ts | Node-only CLI (imports fs). |
| dist/ | Build output (npm run build). Listed in .gitignore; commit after build in CI if needed. |
Build
cd PPS.HandSummaryJS
npm install
npm run buildPublishing to the public npm registry (npmjs.org)
The package name is @jurojinpoker/pps-handsummaryjs. package.json sets publishConfig to registry: https://registry.npmjs.org/ and access: public. The repo PPS.HandSummaryJS/.npmrc pins the @jurojinpoker scope to the same registry.
Installing the package does not require logging in or any npm token; it is a public package on npmjs.
Publishing a new version: you must be a member of the @jurojinpoker org with publish rights. Run npm login for https://registry.npmjs.org/, bump version in package.json, then from PPS.HandSummaryJS:
npm install
npm publish --access publicPowerShell: use npm.cmd if npm hits execution-policy issues.
prepublishOnly runs npm run build so dist/ is fresh.
Consumers
Depend on @jurojinpoker/pps-handsummaryjs with a normal semver range from the public registry. For Next.js apps, list the package under transpilePackages if the app bundles it.
Unit tests (Vitest)
Tests live under test/**/*.test.ts. They exercise parsers, buildHandSummaryFromJson, metrics helpers, and (optionally) the real Aethusa console JSON if the fixture folder exists.
| Command | What it does |
|---------|----------------|
| npm test | Run all tests once (CI-style). |
| npm test -- --reporter=verbose | Same, but prints each test name and timing. |
| npm run test:watch | Re-runs tests when files change. |
Snapshots (*.snap in test/__snapshots__/): some tests use expect(...).toMatchSnapshot(). The first time (or after deleting a .snap), Vitest writes that file; later runs compare the new result to the saved JSON. If you meant to change the output (e.g. pot odds formula), update snapshots on purpose:
npx vitest run -uThen review the diff in Git (and in the .snap file) before committing, so real regressions do not slip through.
Tie a hands.txt hand to unit test output
| What you are looking at | Unit test | How it lines up |
|-------------------------|-----------|-----------------|
| test/fixtures/minimal-hand.json | test/dumpSummary-and-metrics.test.ts | That file is the ingest; the .snap is the expected summary slice for it. |
| test/fixtures/aethusa-console-output/run-*/hands.txt | test/aethusa-console-hands.integration.test.ts | Hands appear in Hands array order: Hands[0] = logical hand #1, same order as the parser wrote them. |
Extract one hand so you can open it next to a test or run buildHandSummaryFromJson on it (from PPS.HandSummaryJS):
# By 1-based index (same as CLI --hand-index)
npm run extract-hand -- ../path/to/hands.txt 17 hand-17.json
# By PokerStars HandId (string after "id:")
npm run extract-hand -- ../path/to/hands.txt id:222296968834 hand-by-id.jsonIf you omit the output file, JSON prints to stdout. Then in the editor use a split view: the extracted hand-17.json on one side and dumpSummary-and-metrics.test.ts on the other (or a tiny scratch test that calls buildHandSummaryFromJson with that file’s text).
Quick locate in the big dump: search the file for "HandId": "…" or use the extract script with id:.
Comfortable compare (input vs JS output, no scrolling the dump): after npm run build, write two small JSON files next to each other — the raw hand from the dump and the same object run through buildHandSummaryFromJson:
npm run hand-inspect -- path/to/hands.txt 17
# or: npm run hand-inspect -- path/to/hands.txt id:222296968834
# optional third arg: output directory (default: .hand-inspect/)You get hand-017-<HandId>-input.json and hand-017-<HandId>-summary.json under .hand-inspect/ (gitignored). Open both in a split editor or your diff tool.
Action-only report (file + stderr path): npm run hand-actions-report — same first two arguments as hand-inspect (dump path + index or id:). Writes PPS.HandSummaryJS/.hand-actions-report/hand-<n>-<HandId>-actions.txt (gitignored). The path is printed on stderr. Optional third argument: output directory; add --stdout to also print the columnar timeline (milestone + label, pot %, MDF %, SPR, …) and the grouped-by-player block. Cursor: /hand-actions-report-cmd.
Breadcrumb JSON: npm run hand-breadcrumb — same arguments as hand-inspect / hand-actions-report. Writes PPS.HandSummaryJS/.hand-breadcrumb/hand-<n>-<HandId>-breadcrumb.json. Folds are dropped on every street except the last one that has action (often river); on that street, folds stay so you see folds to bets/raises. Optional --stdout mirrors the JSON. Cursor: /hand-breadcrumb-cmd.
Visual breadcrumb (table): npm run visual-breadcrumb — same arguments and rules; writes PPS.HandSummaryJS/.visual-breadcrumb/hand-<n>-<HandId>-visual-breadcrumb.txt with a four-column text table (Preflop / Flop / Turn / River), each row the next action in order on that street. Optional --stdout. Cursor: /visual-breadcrumb-cmd.
Run CLI examples
node dist/cli.js --dir ../PPS.SandBox.Console/processedJsons --hand-index 1
node dist/cli.js --file ../PPS.SandBox.Console/processedJsons/run-20260327-173017/hands.txt --hand-index 1Integration test: Aethusa HH → hands.txt → Node
Vitest file: test/aethusa-console-hands.integration.test.ts.
Ingest (raw hand history): PokerStars sample PPS.SandBox.Console/HH/hh.com_Aethusa_289SH_2021.01.06_0.txt (second HH file in that folder; many-player cash hands). The console only accepts a directory of *.txt, so the test workflow uses a folder that contains this file alone.
Generate the processed JSON (from repository root; dotnet writes under PPS.HandSummaryJS/test/fixtures/aethusa-console-output/run-<timestamp>/hands.txt):
mkdir -p .tmp_hh_aethusa_only
cp PPS.SandBox.Console/HH/hh.com_Aethusa_289SH_2021.01.06_0.txt .tmp_hh_aethusa_only/
dotnet run --project PPS.SandBox.Console/PPS.SandBox.Console.csproj -- \
--parse-only \
--save-processed-json \
--processed-json-dir PPS.HandSummaryJS/test/fixtures/aethusa-console-output \
--hh-path .tmp_hh_aethusa_onlyTest flow:
- The test resolves the latest
test/fixtures/aethusa-console-output/run-*/hands.txt(lexicographic sort on folder name). - If that path is missing (e.g. fresh clone; output is gitignored), the whole
describeis skipped — run the command above first. parseProcessedHandsJsonloads the dump (Run+Hands).- One test asserts the expected count of 572 OK hands from that file’s parse-only run.
- Another test runs
buildHandSummaryFromJson(JSON.stringify(hand))for every hand (single-hand wire format the web app uses). - A third test walks all replay summaries and checks that every action with
toCall > 0has non-nullpotOdds(volume check on real parser output).
Note: --parse-only output does not set IsHeroIdentified / hero screen name; MDF hero behavior is covered by the small test/fixtures/minimal-hand.json unit tests.
TypeScript notes
module:nodenext; use.jsextensions in import paths in source (e.g../types.js).src/library code must not importnode:fs/node:path— keep that incli.tsonly.cli.tsuses@types/nodevia the sametsconfig.json.
Related repo docs
.cursor/commands/summary-hand-cmd.md— parser + CLI smoke test..cursor/commands/test-parser-cmd.md— parse failures / diagnostics.
Future ideas
- Optional
--jsonon CLI to emit one hand as JSON. - Stricter runtime validation (e.g. Zod) if the C# schema grows.
- Barrel files per feature area under
src/poker/as logic grows.
