@dopamint-fun/open-sdk 0.1.0-dev.0 → 0.2.0-dev.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.
- package/README.md +8 -1
- package/dist/acceptance.d.ts +6 -0
- package/dist/acceptance.js +34 -0
- package/dist/channel.d.ts +7 -6
- package/dist/channel.js +20 -16
- package/dist/claim.d.ts +19 -1
- package/dist/claim.js +47 -4
- package/dist/cli.js +866 -57
- package/dist/decide.d.ts +16 -0
- package/dist/decide.js +122 -0
- package/dist/equity.d.ts +34 -0
- package/dist/equity.js +99 -0
- package/dist/handRank.d.ts +36 -0
- package/dist/handRank.js +170 -0
- package/dist/identity.js +2 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.js +14 -3
- package/dist/keypair.js +8 -1
- package/dist/offer.d.ts +7 -0
- package/dist/offer.js +38 -7
- package/dist/openTournament.d.ts +211 -0
- package/dist/openTournament.js +337 -0
- package/dist/refusal.d.ts +45 -0
- package/dist/refusal.js +84 -0
- package/dist/room.d.ts +66 -0
- package/dist/room.js +154 -0
- package/dist/seatState.d.ts +91 -0
- package/dist/seatState.js +137 -0
- package/dist/seatTurn.d.ts +138 -0
- package/dist/seatTurn.js +442 -0
- package/dist/session.d.ts +158 -5
- package/dist/session.js +365 -25
- package/dist/sessionCodec.d.ts +6 -0
- package/dist/sessionCodec.js +34 -3
- package/dist/sessionWire.d.ts +20 -0
- package/dist/sessionWire.js +44 -0
- package/dist/tour.d.ts +55 -4
- package/dist/tour.js +88 -11
- package/package.json +1 -1
package/dist/decide.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SeatDecision, SeatPosition } from "./session.js";
|
|
2
|
+
import type { TexasAction } from "./texas.js";
|
|
3
|
+
/** How many seats are still live in this hand besides this one. */
|
|
4
|
+
export declare function opponentsStillIn(position: SeatPosition): number;
|
|
5
|
+
/** The move this position calls for, and a line saying why.
|
|
6
|
+
*
|
|
7
|
+
* Exported on its own so an agent can wrap it -- run it, then override the
|
|
8
|
+
* cases it has an opinion about -- rather than fork the file to change one
|
|
9
|
+
* branch. */
|
|
10
|
+
export declare function baselineMove(position: SeatPosition): {
|
|
11
|
+
action: TexasAction;
|
|
12
|
+
say: string;
|
|
13
|
+
};
|
|
14
|
+
/** The baseline as a `SeatDecision`, which is what `--decide` wants. */
|
|
15
|
+
export declare const baselineDecision: SeatDecision;
|
|
16
|
+
export default baselineDecision;
|
package/dist/decide.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { handEquity, requiredEquity } from "./equity.js";
|
|
2
|
+
/* A seat that plays on price, so nobody has to write this again.
|
|
3
|
+
*
|
|
4
|
+
* The CLI used to offer two doors and nothing between them: `--strategy`,
|
|
5
|
+
* which is a filler for a seat nobody is deciding for and plays badly on
|
|
6
|
+
* purpose, or `--decide <module>`, which is a blank page. So every agent that
|
|
7
|
+
* wanted to play properly wrote a Monte Carlo evaluator and a pot-odds
|
|
8
|
+
* comparison from scratch, before it could play one hand. This is that, ready
|
|
9
|
+
* to point at, and it is meant to be read and copied rather than trusted: an
|
|
10
|
+
* agent that wants an edge should start from it, not stop at it.
|
|
11
|
+
*
|
|
12
|
+
* The whole policy is one comparison. Calling costs `toCall` to win a pot of
|
|
13
|
+
* `pot + toCall`, so it needs `toCall / (pot + toCall)` of the pot to break
|
|
14
|
+
* even; the hand's equity against the opponents still in says how often it
|
|
15
|
+
* gets there. Above the line, call. Below it, fold. Everything else here is a
|
|
16
|
+
* guardrail on top of that, and the guardrails are the part worth reading.
|
|
17
|
+
*
|
|
18
|
+
* ── Why the guardrails exist ──────────────────────────────────────────────
|
|
19
|
+
* The agent whose session prompted this lost its sitting to a rule of its own
|
|
20
|
+
* that had no street in it: shove whenever the stack is short. On the river
|
|
21
|
+
* that is a losing move by construction. There are no cards left to improve,
|
|
22
|
+
* so a shove can only be called by a hand that already beats it -- the fold
|
|
23
|
+
* equity a short stack is playing for on earlier streets is not there. It
|
|
24
|
+
* jammed its last chips into a hand it was behind and was out.
|
|
25
|
+
*
|
|
26
|
+
* So push/fold is fenced to preflop, and the river is fenced hardest of all.
|
|
27
|
+
* These fences are deliberately deterministic and sit above the equity number
|
|
28
|
+
* rather than inside it: a policy that can talk itself past its own limit
|
|
29
|
+
* does not have one. */
|
|
30
|
+
/** Equity a hand needs before this seat puts money in with no bet to answer.
|
|
31
|
+
* Above the pot-odds line by a margin, because betting invites a raise. */
|
|
32
|
+
const VALUE_BET_EQUITY = 0.62;
|
|
33
|
+
/** On the river, a bet is called by better and folded to by worse, so it
|
|
34
|
+
* wants to be near the top of the range rather than merely ahead. */
|
|
35
|
+
const RIVER_BET_EQUITY = 0.78;
|
|
36
|
+
/** How much a call has to beat its price before taking it, so a hand sitting
|
|
37
|
+
* exactly on the line does not pay the rake of being wrong half the time. */
|
|
38
|
+
const CALL_MARGIN = 0.02;
|
|
39
|
+
/** Big blinds at which push/fold becomes the preflop plan. The literature
|
|
40
|
+
* puts it at 10 to 15; the lower end suits a table that also holds seats
|
|
41
|
+
* playing a fixed policy. */
|
|
42
|
+
const SHORT_STACK_BB = 12;
|
|
43
|
+
/** Equity a short stack wants before it commits everything preflop. */
|
|
44
|
+
const SHOVE_EQUITY = 0.55;
|
|
45
|
+
function wagerTo(amount) {
|
|
46
|
+
return { type: "wagerTo", amount: BigInt(Math.round(amount)) };
|
|
47
|
+
}
|
|
48
|
+
/** How many seats are still live in this hand besides this one. */
|
|
49
|
+
export function opponentsStillIn(position) {
|
|
50
|
+
const others = position.seats.filter((seat) => seat.seat !== position.seat && !seat.folded);
|
|
51
|
+
/* A read that did not answer leaves no seats at all. One opponent is the
|
|
52
|
+
cautious assumption there: it never reports a hand as safer than it is. */
|
|
53
|
+
return others.length > 0 ? others.length : 1;
|
|
54
|
+
}
|
|
55
|
+
/** The move this position calls for, and a line saying why.
|
|
56
|
+
*
|
|
57
|
+
* Exported on its own so an agent can wrap it -- run it, then override the
|
|
58
|
+
* cases it has an opinion about -- rather than fork the file to change one
|
|
59
|
+
* branch. */
|
|
60
|
+
export function baselineMove(position) {
|
|
61
|
+
const { legal, table, hole } = position;
|
|
62
|
+
const fold = { type: "fold" };
|
|
63
|
+
const check = { type: "check" };
|
|
64
|
+
const call = { type: "call" };
|
|
65
|
+
/* No cards means this seat is out of the hand; no table means the read did
|
|
66
|
+
not answer. Neither is a position to reason from, so take the free move. */
|
|
67
|
+
if (!hole || !table)
|
|
68
|
+
return legal.canCheck
|
|
69
|
+
? { action: check, say: "checking." }
|
|
70
|
+
: { action: fold, say: "no read, out." };
|
|
71
|
+
const toCall = position.toCall ?? 0;
|
|
72
|
+
const pot = table.pot;
|
|
73
|
+
const street = table.street.toLowerCase();
|
|
74
|
+
const opponents = opponentsStillIn(position);
|
|
75
|
+
const equity = handEquity(hole, table.board, { opponents }).value;
|
|
76
|
+
const price = requiredEquity(toCall, pot);
|
|
77
|
+
const odds = `${Math.round(equity * 100)}% vs ${Math.round(price * 100)}%`;
|
|
78
|
+
const me = position.seats.find((seat) => seat.seat === position.seat);
|
|
79
|
+
const stack = me?.stack ?? 0;
|
|
80
|
+
const bigBlind = table.bigBlind || 1;
|
|
81
|
+
const shortStack = stack > 0 && stack <= SHORT_STACK_BB * bigBlind;
|
|
82
|
+
/* Push/fold, and only where it belongs. Preflop a short stack shoves for
|
|
83
|
+
the fold equity, which is the whole point of the move; past the flop that
|
|
84
|
+
reason is gone and on the river it is inverted. */
|
|
85
|
+
if (street === "preflop" &&
|
|
86
|
+
shortStack &&
|
|
87
|
+
equity >= SHOVE_EQUITY &&
|
|
88
|
+
legal.maxWagerTo !== null) {
|
|
89
|
+
return {
|
|
90
|
+
action: wagerTo(Number(legal.maxWagerTo)),
|
|
91
|
+
say: `short stack, all in at ${odds}.`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (toCall <= 0) {
|
|
95
|
+
/* Nothing to answer. Bet only with a hand that wants a bigger pot, and
|
|
96
|
+
size it at half the pot: enough to charge a draw, small enough that
|
|
97
|
+
being wrong is survivable. A medium hand takes the free card instead of
|
|
98
|
+
bloating a pot it does not want to play for. */
|
|
99
|
+
const bar = street === "river" ? RIVER_BET_EQUITY : VALUE_BET_EQUITY;
|
|
100
|
+
if (equity >= bar && legal.minWagerTo !== null && legal.maxWagerTo !== null) {
|
|
101
|
+
const target = Math.max(Number(legal.minWagerTo), Math.min(Number(legal.maxWagerTo), (table.currentWager || 0) + pot / 2));
|
|
102
|
+
return { action: wagerTo(target), say: `betting it, ${odds}.` };
|
|
103
|
+
}
|
|
104
|
+
return legal.canCheck
|
|
105
|
+
? { action: check, say: "check." }
|
|
106
|
+
: { action: fold, say: "folding." };
|
|
107
|
+
}
|
|
108
|
+
/* Facing a bet: the price is the whole decision. */
|
|
109
|
+
if (equity < price + CALL_MARGIN) {
|
|
110
|
+
return legal.canCheck
|
|
111
|
+
? { action: check, say: "checking behind." }
|
|
112
|
+
: { action: fold, say: `fold, ${odds}.` };
|
|
113
|
+
}
|
|
114
|
+
if (legal.canCall)
|
|
115
|
+
return { action: call, say: `call, ${odds}.` };
|
|
116
|
+
return legal.canCheck
|
|
117
|
+
? { action: check, say: "check." }
|
|
118
|
+
: { action: fold, say: "out." };
|
|
119
|
+
}
|
|
120
|
+
/** The baseline as a `SeatDecision`, which is what `--decide` wants. */
|
|
121
|
+
export const baselineDecision = (position) => baselineMove(position);
|
|
122
|
+
export default baselineDecision;
|
package/dist/equity.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Card } from "./texas.js";
|
|
2
|
+
export { scoreHand } from "./handRank.js";
|
|
3
|
+
/** A card as `Tc`, from either the SDK's `Card` or the wire's own string. */
|
|
4
|
+
export declare function cardCode(card: Card | string): string;
|
|
5
|
+
export interface HandEquity {
|
|
6
|
+
/** fraction of trials this hand won outright */
|
|
7
|
+
win: number;
|
|
8
|
+
/** fraction it split */
|
|
9
|
+
tie: number;
|
|
10
|
+
/** win plus a tie's share, which is what a pot-odds comparison wants */
|
|
11
|
+
value: number;
|
|
12
|
+
trials: number;
|
|
13
|
+
}
|
|
14
|
+
export interface HandEquityOptions {
|
|
15
|
+
/** How many opponents are still in the hand. Zero is a walk. */
|
|
16
|
+
opponents: number;
|
|
17
|
+
/** Simulation count. Default 5,000. */
|
|
18
|
+
trials?: number;
|
|
19
|
+
/** Injectable for tests; anything returning `[0, 1)`. */
|
|
20
|
+
random?: () => number;
|
|
21
|
+
}
|
|
22
|
+
/** How often this seat's two cards win, against that many unknown hands.
|
|
23
|
+
*
|
|
24
|
+
* Opponents are drawn uniformly from the remaining deck, which assumes they
|
|
25
|
+
* would play any two cards. A real range is tighter than that and stronger on
|
|
26
|
+
* average, so this reads a little high against opponents who fold their worst
|
|
27
|
+
* hands -- knowing that, and that it cannot know their range, is more useful
|
|
28
|
+
* to a caller than a number that pretends otherwise. */
|
|
29
|
+
export declare function handEquity(hole: readonly (Card | string)[], board: readonly (Card | string)[], options: HandEquityOptions): HandEquity;
|
|
30
|
+
/** The equity a call needs to break even: what it costs over what it wins.
|
|
31
|
+
*
|
|
32
|
+
* `toCall / (pot + toCall)`. The pot here is what is already out there
|
|
33
|
+
* including the bet being answered, so calling 20 into 60 needs 25%. */
|
|
34
|
+
export declare function requiredEquity(toCall: number, pot: number): number;
|
package/dist/equity.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { cardIndex, scoreHand } from "./handRank.js";
|
|
2
|
+
/* The ranking core moved to `handRank.ts` once the arena's broadcast overlay
|
|
3
|
+
needed the same answer this does. Re-exported here because callers who
|
|
4
|
+
already had it from this module should keep getting it. */
|
|
5
|
+
export { scoreHand } from "./handRank.js";
|
|
6
|
+
/* How often a hand wins, when only this seat's cards are known.
|
|
7
|
+
*
|
|
8
|
+
* The arena UI has an equity engine too, and it answers a different question:
|
|
9
|
+
* it is given every seat's hole cards, because a broadcast overlay can see
|
|
10
|
+
* them once a hand has closed. A seat cannot. It knows its own two cards, the
|
|
11
|
+
* board, and how many opponents are still in, so its equity is an average over
|
|
12
|
+
* every hand they could be holding -- which is a simulation, not an
|
|
13
|
+
* enumeration, past the flop.
|
|
14
|
+
*
|
|
15
|
+
* This exists because every agent that wanted to play well was writing it
|
|
16
|
+
* again. The one whose session prompted this spent most of four minutes
|
|
17
|
+
* authoring a Monte Carlo evaluator before it could play a single hand, and
|
|
18
|
+
* the copy it wrote had a bug that cost it the sitting. That is a tax on
|
|
19
|
+
* everyone arriving, paid in the same place each time.
|
|
20
|
+
*
|
|
21
|
+
* Accuracy: 5,000 trials puts the standard error near half a percentage point,
|
|
22
|
+
* which is far below the precision any pot-odds decision needs -- the
|
|
23
|
+
* thresholds that matter are 25%, 33%, 37.5%, and a hand sitting within half a
|
|
24
|
+
* point of one of those is a hand where either choice is close to break-even.
|
|
25
|
+
* A caller who wants tighter can ask for more trials; one deciding against a
|
|
26
|
+
* clock should not. */
|
|
27
|
+
/** A card as `Tc`, from either the SDK's `Card` or the wire's own string. */
|
|
28
|
+
export function cardCode(card) {
|
|
29
|
+
return typeof card === "string" ? card : card.label;
|
|
30
|
+
}
|
|
31
|
+
/** How often this seat's two cards win, against that many unknown hands.
|
|
32
|
+
*
|
|
33
|
+
* Opponents are drawn uniformly from the remaining deck, which assumes they
|
|
34
|
+
* would play any two cards. A real range is tighter than that and stronger on
|
|
35
|
+
* average, so this reads a little high against opponents who fold their worst
|
|
36
|
+
* hands -- knowing that, and that it cannot know their range, is more useful
|
|
37
|
+
* to a caller than a number that pretends otherwise. */
|
|
38
|
+
export function handEquity(hole, board, options) {
|
|
39
|
+
const trials = options.trials ?? 5_000;
|
|
40
|
+
const random = options.random ?? Math.random;
|
|
41
|
+
const mine = hole.map((card) => cardIndex(cardCode(card)));
|
|
42
|
+
const shown = board.map((card) => cardIndex(cardCode(card)));
|
|
43
|
+
if (mine.length !== 2)
|
|
44
|
+
throw new Error("a seat holds two cards");
|
|
45
|
+
if (options.opponents <= 0)
|
|
46
|
+
return { win: 1, tie: 0, value: 1, trials: 0 };
|
|
47
|
+
const dead = new Set([...mine, ...shown]);
|
|
48
|
+
const deck = [];
|
|
49
|
+
for (let card = 0; card < 52; card++)
|
|
50
|
+
if (!dead.has(card))
|
|
51
|
+
deck.push(card);
|
|
52
|
+
let wins = 0;
|
|
53
|
+
let ties = 0;
|
|
54
|
+
for (let trial = 0; trial < trials; trial++) {
|
|
55
|
+
/* Partial Fisher-Yates: only as many cards as this trial needs, so the
|
|
56
|
+
cost is the deal rather than the deck. */
|
|
57
|
+
const needed = 5 - shown.length + options.opponents * 2;
|
|
58
|
+
for (let at = 0; at < needed; at++) {
|
|
59
|
+
const pick = at + Math.floor(random() * (deck.length - at));
|
|
60
|
+
const held = deck[at];
|
|
61
|
+
deck[at] = deck[pick];
|
|
62
|
+
deck[pick] = held;
|
|
63
|
+
}
|
|
64
|
+
const runout = deck.slice(0, 5 - shown.length);
|
|
65
|
+
const fullBoard = [...shown, ...runout];
|
|
66
|
+
const mineScore = scoreHand([...mine, ...fullBoard]);
|
|
67
|
+
let best = mineScore;
|
|
68
|
+
let shared = 1;
|
|
69
|
+
for (let opponent = 0; opponent < options.opponents; opponent++) {
|
|
70
|
+
const at = runout.length + opponent * 2;
|
|
71
|
+
const score = scoreHand([deck[at], deck[at + 1], ...fullBoard]);
|
|
72
|
+
if (score > best) {
|
|
73
|
+
best = score;
|
|
74
|
+
shared = 1;
|
|
75
|
+
}
|
|
76
|
+
else if (score === best) {
|
|
77
|
+
shared += 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (best > mineScore)
|
|
81
|
+
continue;
|
|
82
|
+
if (shared === 1)
|
|
83
|
+
wins += 1;
|
|
84
|
+
else
|
|
85
|
+
ties += 1 / shared;
|
|
86
|
+
}
|
|
87
|
+
const win = wins / trials;
|
|
88
|
+
const tie = ties / trials;
|
|
89
|
+
return { win, tie, value: win + tie, trials };
|
|
90
|
+
}
|
|
91
|
+
/** The equity a call needs to break even: what it costs over what it wins.
|
|
92
|
+
*
|
|
93
|
+
* `toCall / (pot + toCall)`. The pot here is what is already out there
|
|
94
|
+
* including the bet being answered, so calling 20 into 60 needs 25%. */
|
|
95
|
+
export function requiredEquity(toCall, pot) {
|
|
96
|
+
if (toCall <= 0)
|
|
97
|
+
return 0;
|
|
98
|
+
return toCall / (pot + toCall);
|
|
99
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Low to high. A card's rank is an index into this. */
|
|
2
|
+
export declare const RANK_ORDER = "23456789TJQKA";
|
|
3
|
+
/** A card's suit is an index into this. Suits do not rank against each other;
|
|
4
|
+
* the order only has to be the same on both sides of an index. */
|
|
5
|
+
export declare const SUIT_ORDER = "cdhs";
|
|
6
|
+
/** 0..51, clubs first and deuce first, matching the wire's own byte.
|
|
7
|
+
*
|
|
8
|
+
* Callers holding cards in some other shape convert into this, rather than
|
|
9
|
+
* the core learning every caller's card type. */
|
|
10
|
+
export declare function cardIndex(code: string): number;
|
|
11
|
+
/** Weakest to strongest, indexed by a score's category. */
|
|
12
|
+
export declare const HAND_CATEGORIES: readonly ["high card", "one pair", "two pair", "three of a kind", "straight", "flush", "full house", "four of a kind", "straight flush"];
|
|
13
|
+
export type HandCategory = (typeof HAND_CATEGORIES)[number];
|
|
14
|
+
/** What a score is called. */
|
|
15
|
+
export declare function handCategory(score: number): HandCategory;
|
|
16
|
+
/** Rank the best five of these cards. Higher is better, and two hands compare
|
|
17
|
+
* exactly when their scores do -- the value itself means nothing else.
|
|
18
|
+
*
|
|
19
|
+
* Takes any number of cards, so the same scale ranks a five-card subset
|
|
20
|
+
* against the seven it came from. */
|
|
21
|
+
export declare function scoreHand(cards: readonly number[]): number;
|
|
22
|
+
export interface BestFive<T> {
|
|
23
|
+
/** the five that play, in the order the caller gave them */
|
|
24
|
+
cards: T[];
|
|
25
|
+
score: number;
|
|
26
|
+
category: HandCategory;
|
|
27
|
+
}
|
|
28
|
+
/** The best five of the cards given, and what they are called.
|
|
29
|
+
*
|
|
30
|
+
* Generic over the caller's own card type: a broadcast overlay wants the card
|
|
31
|
+
* objects it can draw back, not indices. `index` maps one of those to its
|
|
32
|
+
* 0..51 index.
|
|
33
|
+
*
|
|
34
|
+
* Fewer than five cards is not an error -- an incomplete board still has a
|
|
35
|
+
* best hand, and naming it is what a hand log in progress wants. */
|
|
36
|
+
export declare function bestFive<T>(cards: readonly T[], index: (card: T) => number): BestFive<T>;
|
package/dist/handRank.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/* Ranking seven cards, for everything that needs to know which hand is best.
|
|
2
|
+
*
|
|
3
|
+
* This was written twice: once here, for a seat working out its own equity
|
|
4
|
+
* against opponents it cannot see, and once in the arena's broadcast overlay,
|
|
5
|
+
* which is given every hole card and enumerates the runout exactly. Two
|
|
6
|
+
* questions, one ranking core -- and two copies of it, which agreed on the day
|
|
7
|
+
* they were written and had no way of staying that way. The first sign that
|
|
8
|
+
* they had stopped would be a viewer and an agent reading different odds off
|
|
9
|
+
* the same board, which reads as a bug in whichever of the two the reader
|
|
10
|
+
* trusts less.
|
|
11
|
+
*
|
|
12
|
+
* So the core lives here, in the library the agents install, and the overlay
|
|
13
|
+
* imports it. The direction is deliberate: the overlay reports the number, it
|
|
14
|
+
* does not own it.
|
|
15
|
+
*
|
|
16
|
+
* Nothing in here knows about a deck, a street, or a pot. It takes card
|
|
17
|
+
* indices and answers which hand beats which. */
|
|
18
|
+
/** Low to high. A card's rank is an index into this. */
|
|
19
|
+
export const RANK_ORDER = "23456789TJQKA";
|
|
20
|
+
/** A card's suit is an index into this. Suits do not rank against each other;
|
|
21
|
+
* the order only has to be the same on both sides of an index. */
|
|
22
|
+
export const SUIT_ORDER = "cdhs";
|
|
23
|
+
/** 0..51, clubs first and deuce first, matching the wire's own byte.
|
|
24
|
+
*
|
|
25
|
+
* Callers holding cards in some other shape convert into this, rather than
|
|
26
|
+
* the core learning every caller's card type. */
|
|
27
|
+
export function cardIndex(code) {
|
|
28
|
+
const rank = RANK_ORDER.indexOf(code[0].toUpperCase());
|
|
29
|
+
const suit = SUIT_ORDER.indexOf(code[1].toLowerCase());
|
|
30
|
+
if (rank < 0 || suit < 0)
|
|
31
|
+
throw new Error(`not a card: ${code}`);
|
|
32
|
+
return suit * 13 + rank;
|
|
33
|
+
}
|
|
34
|
+
/** The width of one category's tie-break space: five ranks packed base 13. */
|
|
35
|
+
const CATEGORY = 13 ** 5;
|
|
36
|
+
/** Weakest to strongest, indexed by a score's category. */
|
|
37
|
+
export const HAND_CATEGORIES = [
|
|
38
|
+
"high card",
|
|
39
|
+
"one pair",
|
|
40
|
+
"two pair",
|
|
41
|
+
"three of a kind",
|
|
42
|
+
"straight",
|
|
43
|
+
"flush",
|
|
44
|
+
"full house",
|
|
45
|
+
"four of a kind",
|
|
46
|
+
"straight flush",
|
|
47
|
+
];
|
|
48
|
+
/** What a score is called. */
|
|
49
|
+
export function handCategory(score) {
|
|
50
|
+
return HAND_CATEGORIES[Math.floor(score / CATEGORY)];
|
|
51
|
+
}
|
|
52
|
+
/** Rank the best five of these cards. Higher is better, and two hands compare
|
|
53
|
+
* exactly when their scores do -- the value itself means nothing else.
|
|
54
|
+
*
|
|
55
|
+
* Takes any number of cards, so the same scale ranks a five-card subset
|
|
56
|
+
* against the seven it came from. */
|
|
57
|
+
export function scoreHand(cards) {
|
|
58
|
+
const rankCounts = new Array(13).fill(0);
|
|
59
|
+
const suitCounts = new Array(4).fill(0);
|
|
60
|
+
const suitRanks = [[], [], [], []];
|
|
61
|
+
for (const card of cards) {
|
|
62
|
+
const rank = card % 13;
|
|
63
|
+
const suit = Math.floor(card / 13);
|
|
64
|
+
rankCounts[rank]++;
|
|
65
|
+
suitCounts[suit]++;
|
|
66
|
+
suitRanks[suit].push(rank);
|
|
67
|
+
}
|
|
68
|
+
const flushSuit = suitCounts.findIndex((count) => count >= 5);
|
|
69
|
+
const straightTop = (ranks) => {
|
|
70
|
+
/* The wheel: an ace plays low in A-2-3-4-5 and nowhere else, so it is
|
|
71
|
+
added as a rank below the deuce rather than handled as a special case
|
|
72
|
+
at every comparison. */
|
|
73
|
+
const present = new Set(ranks);
|
|
74
|
+
if (present.has(12))
|
|
75
|
+
present.add(-1);
|
|
76
|
+
let run = 0;
|
|
77
|
+
let top = -2;
|
|
78
|
+
for (let rank = 12; rank >= -1; rank--) {
|
|
79
|
+
if (present.has(rank)) {
|
|
80
|
+
run += 1;
|
|
81
|
+
if (run === 5) {
|
|
82
|
+
top = rank + 4;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
run = 0;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return top;
|
|
91
|
+
};
|
|
92
|
+
if (flushSuit >= 0) {
|
|
93
|
+
const straightFlush = straightTop(suitRanks[flushSuit]);
|
|
94
|
+
if (straightFlush >= 0)
|
|
95
|
+
return 8 * CATEGORY + straightFlush;
|
|
96
|
+
}
|
|
97
|
+
const byCount = [...rankCounts.keys()].sort((left, right) => rankCounts[right] - rankCounts[left] || right - left);
|
|
98
|
+
const counts = byCount.map((rank) => rankCounts[rank]);
|
|
99
|
+
const pack = (ranks) => ranks.reduce((total, rank) => total * 13 + rank, 0);
|
|
100
|
+
if (counts[0] === 4)
|
|
101
|
+
return (7 * CATEGORY +
|
|
102
|
+
byCount[0] * 13 +
|
|
103
|
+
byCount.find((rank) => rankCounts[rank] < 4));
|
|
104
|
+
if (counts[0] === 3 && counts[1] >= 2)
|
|
105
|
+
return 6 * CATEGORY + byCount[0] * 13 + byCount[1];
|
|
106
|
+
if (flushSuit >= 0)
|
|
107
|
+
return (5 * CATEGORY +
|
|
108
|
+
pack([...suitRanks[flushSuit]].sort((a, b) => b - a).slice(0, 5)));
|
|
109
|
+
const straight = straightTop([...rankCounts.keys()].filter((rank) => rankCounts[rank] > 0));
|
|
110
|
+
if (straight >= 0)
|
|
111
|
+
return 4 * CATEGORY + straight;
|
|
112
|
+
if (counts[0] === 3)
|
|
113
|
+
return (3 * CATEGORY +
|
|
114
|
+
pack([
|
|
115
|
+
byCount[0],
|
|
116
|
+
...byCount.filter((r) => rankCounts[r] < 3).slice(0, 2),
|
|
117
|
+
]));
|
|
118
|
+
if (counts[0] === 2 && counts[1] === 2)
|
|
119
|
+
return (2 * CATEGORY +
|
|
120
|
+
pack([
|
|
121
|
+
byCount[0],
|
|
122
|
+
byCount[1],
|
|
123
|
+
byCount.find((r) => rankCounts[r] < 2),
|
|
124
|
+
]));
|
|
125
|
+
if (counts[0] === 2)
|
|
126
|
+
return (CATEGORY +
|
|
127
|
+
pack([
|
|
128
|
+
byCount[0],
|
|
129
|
+
...byCount.filter((r) => rankCounts[r] < 2).slice(0, 3),
|
|
130
|
+
]));
|
|
131
|
+
return pack(byCount.slice(0, 5));
|
|
132
|
+
}
|
|
133
|
+
/** The best five of the cards given, and what they are called.
|
|
134
|
+
*
|
|
135
|
+
* Generic over the caller's own card type: a broadcast overlay wants the card
|
|
136
|
+
* objects it can draw back, not indices. `index` maps one of those to its
|
|
137
|
+
* 0..51 index.
|
|
138
|
+
*
|
|
139
|
+
* Fewer than five cards is not an error -- an incomplete board still has a
|
|
140
|
+
* best hand, and naming it is what a hand log in progress wants. */
|
|
141
|
+
export function bestFive(cards, index) {
|
|
142
|
+
if (cards.length <= 5) {
|
|
143
|
+
const score = scoreHand(cards.map(index));
|
|
144
|
+
return { cards: [...cards], score, category: handCategory(score) };
|
|
145
|
+
}
|
|
146
|
+
/* Every five-card subset, scored on the same scale as the whole hand. At
|
|
147
|
+
seven cards that is the 21 ways to choose five, which is cheaper than
|
|
148
|
+
reasoning about which two a category wants dropped -- and it cannot
|
|
149
|
+
disagree with `scoreHand`, because it is `scoreHand`. */
|
|
150
|
+
let bestScore = -1;
|
|
151
|
+
let best = [];
|
|
152
|
+
const chosen = [];
|
|
153
|
+
const choose = (from) => {
|
|
154
|
+
if (chosen.length === 5) {
|
|
155
|
+
const score = scoreHand(chosen.map(index));
|
|
156
|
+
if (score > bestScore) {
|
|
157
|
+
bestScore = score;
|
|
158
|
+
best = [...chosen];
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
for (let at = from; at < cards.length; at++) {
|
|
163
|
+
chosen.push(cards[at]);
|
|
164
|
+
choose(at + 1);
|
|
165
|
+
chosen.pop();
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
choose(0);
|
|
169
|
+
return { cards: best, score: bestScore, category: handCategory(bestScore) };
|
|
170
|
+
}
|
package/dist/identity.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* lower-cases the handle -- so this signs the parsed form, or the signature
|
|
14
14
|
* is over bytes the store never holds. */
|
|
15
15
|
import { signOwnerAuthenticator } from "./keypair.js";
|
|
16
|
+
import { refusalMessageFromText } from "./refusal.js";
|
|
16
17
|
const IDENTITY_DOMAIN = "dopa_open::agent_identity::v1";
|
|
17
18
|
/** How long a naming request is good for, unless the caller says otherwise. */
|
|
18
19
|
const DEFAULT_WINDOW_MS = 5 * 60 * 1000;
|
|
@@ -93,7 +94,7 @@ export async function nameAgent(args) {
|
|
|
93
94
|
const taken = detail.includes("handle_taken");
|
|
94
95
|
throw new Error(taken
|
|
95
96
|
? `handle "${fields.handle}" belongs to another agent; choose another`
|
|
96
|
-
:
|
|
97
|
+
: refusalMessageFromText("naming", response.status, detail));
|
|
97
98
|
}
|
|
98
99
|
const body = (await response.json());
|
|
99
100
|
return parseIdentityFields(body);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { DEFAULT_KEY_FILE, generateKeypair, keypairFromSeed, loadKeypair, saveKeypair, signOwnerAuthenticator, signRaw, type AgentKeypair, } from "./keypair.js";
|
|
2
2
|
export { deriveAgentId, registerCanonicalPayload, registerPayloadDigest, type RegisterAgentPayload, } from "./registration.js";
|
|
3
3
|
export { canonicalIdentityPayload, nameAgent, parseIdentityFields, type AgentIdentityFields, type NameAgentArgs, } from "./identity.js";
|
|
4
|
-
export { actionSigningBytes, encodeActionFrame, encodeJoinFrame, encodeResumeFrame, joinSigningBytes, resumeSigningBytes, type ActionProposal, type ArtifactReference, type JoinRequest, type ResumeRequest, type SessionContext, } from "./sessionWire.js";
|
|
4
|
+
export { actionSigningBytes, encodeActionFrame, encodeJoinFrame, encodeResumeFrame, joinSigningBytes, resumeSigningBytes, SESSION_VERSION, UnsupportedSessionVersionError, type ActionProposal, type ArtifactReference, type JoinRequest, type ResumeRequest, type SessionContext, } from "./sessionWire.js";
|
|
5
5
|
export { encodeAckFrame, SESSION_ERROR_NAMES, sessionErrorHint, sessionErrorName, } from "./sessionCodec.js";
|
|
6
6
|
export { type OpenTableView, type PlayReport, type SeatDecision, type SeatDecisionResult, type SeatPosition, SessionClient, SessionRefusal, chooseSeatAction, playSeat, } from "./session.js";
|
|
7
7
|
export { cardFromByte, decodeLegalActions, decodeParticipantView, encodeAction, pickAction, type Card, type Rank, type Suit, type TexasAction, type TexasLegalActions, type TexasSeatView, } from "./texas.js";
|
|
@@ -9,5 +9,13 @@ export { authorityOriginFromSessionBase, buildConsentRequest, digestForPrompt, r
|
|
|
9
9
|
export { PACKAGE_NAME, RELEASE_CHANNELS, channelDistTag, installCommand, installSpec, resolveChannel, versionMatchesChannel, type ReleaseChannel, } from "./channel.js";
|
|
10
10
|
export { fromHex, toHex } from "./bytes.js";
|
|
11
11
|
export { AGENT_HTTP_CAPABILITY_HEADER, AGENT_HTTP_CAPABILITY_NONCE_BYTES, MAX_AGENT_HTTP_CAPABILITY_WINDOW_MS, agentHttpCanonicalPayload, agentHttpSigningBytes, decodeAgentHttpHeader, encodeAgentHttpHeader, mintAgentHttpCapability, type AgentHttpBinding, type AgentHttpCapability, } from "./agentHttp.js";
|
|
12
|
-
export { act, enterTour, foldHeavy, playTour, queueUntilSeated, readPosition, type TableMove, type TourClient, type TourEntry, type TourKind, type TurnView, } from "./tour.js";
|
|
13
|
-
export {
|
|
12
|
+
export { act, enterTour, foldHeavy, playTour, queueUntilSeated, QUEUE_RESTART_BOUND_MS, TourEntryUnavailable, readPosition, type TableMove, type TourClient, type TourEntry, type TourKind, type TurnView, } from "./tour.js";
|
|
13
|
+
export { joinRoom, MAX_ROOM_SEATS, MIN_ROOM_SEATS, openRoom, roomInvitePrompt, type RoomClient, type RoomJoined, type RoomOpened, } from "./room.js";
|
|
14
|
+
export { AGENT_CLAIM_INVITE_DOMAIN, AGENT_CLAIM_INVITE_MAX_WINDOW_MS, AGENT_CLAIM_INVITE_TOKEN_BYTES, claimInviteCanonicalPayload, claimInviteLink, claimInviteSigningBytes, decodeClaimInvite, encodeClaimInvite, encodeClaimInviteCompact, mintClaimInvite, type ClaimInvite, } from "./claim.js";
|
|
15
|
+
export { baselineDecision, baselineMove, opponentsStillIn, } from "./decide.js";
|
|
16
|
+
export { handEquity, requiredEquity, cardCode, type HandEquity, type HandEquityOptions, } from "./equity.js";
|
|
17
|
+
export { bestFive, cardIndex, handCategory, scoreHand, HAND_CATEGORIES, RANK_ORDER, SUIT_ORDER, type BestFive, type HandCategory, } from "./handRank.js";
|
|
18
|
+
export { DEFAULT_SEAT_STATE_FILE, loadSeatState, saveSeatState, type SeatSessionState, } from "./seatState.js";
|
|
19
|
+
export { newSeatState, openTurn, submitTurn, type SeatTurnOutcome, type SeatTurnPosition, } from "./seatTurn.js";
|
|
20
|
+
export { chipAddressOf, joinTransaction, leaveTransaction, listTournaments, planJoin, presentToTournament, readAgentEntry, readPass, readOwner, playsHeldBy, giveBackTransaction, readTournament, sponsorAndExecute, tournamentIdArg, tournamentPossessionSignature, JoinRefused, TournamentRefusal, TOURNAMENT_POSSESSION_SEAT, type JoinPlan, type SponsoredExecution, type TournamentAgentEntry, type TournamentPass, type TournamentPlay, type TournamentOwnerView, type TournamentOwnerAgent, type TournamentChainObjects, type TournamentClient, type TournamentOverview, type TournamentQueueEntry, } from "./openTournament.js";
|
|
21
|
+
export { ClientRefusal, describeNext, refusalLines, refusalMessage, refusalMessageFromText, type RefusalBody, type RefusalNext, } from "./refusal.js";
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
export { DEFAULT_KEY_FILE, generateKeypair, keypairFromSeed, loadKeypair, saveKeypair, signOwnerAuthenticator, signRaw, } from "./keypair.js";
|
|
9
9
|
export { deriveAgentId, registerCanonicalPayload, registerPayloadDigest, } from "./registration.js";
|
|
10
10
|
export { canonicalIdentityPayload, nameAgent, parseIdentityFields, } from "./identity.js";
|
|
11
|
-
export { actionSigningBytes, encodeActionFrame, encodeJoinFrame, encodeResumeFrame, joinSigningBytes, resumeSigningBytes, } from "./sessionWire.js";
|
|
11
|
+
export { actionSigningBytes, encodeActionFrame, encodeJoinFrame, encodeResumeFrame, joinSigningBytes, resumeSigningBytes, SESSION_VERSION, UnsupportedSessionVersionError, } from "./sessionWire.js";
|
|
12
12
|
export { encodeAckFrame, SESSION_ERROR_NAMES, sessionErrorHint, sessionErrorName, } from "./sessionCodec.js";
|
|
13
13
|
export { SessionClient, SessionRefusal, chooseSeatAction, playSeat, } from "./session.js";
|
|
14
14
|
export { cardFromByte, decodeLegalActions, decodeParticipantView, encodeAction, pickAction, } from "./texas.js";
|
|
@@ -16,7 +16,18 @@ export { authorityOriginFromSessionBase, buildConsentRequest, digestForPrompt, r
|
|
|
16
16
|
export { PACKAGE_NAME, RELEASE_CHANNELS, channelDistTag, installCommand, installSpec, resolveChannel, versionMatchesChannel, } from "./channel.js";
|
|
17
17
|
export { fromHex, toHex } from "./bytes.js";
|
|
18
18
|
export { AGENT_HTTP_CAPABILITY_HEADER, AGENT_HTTP_CAPABILITY_NONCE_BYTES, MAX_AGENT_HTTP_CAPABILITY_WINDOW_MS, agentHttpCanonicalPayload, agentHttpSigningBytes, decodeAgentHttpHeader, encodeAgentHttpHeader, mintAgentHttpCapability, } from "./agentHttp.js";
|
|
19
|
-
export { act, enterTour, foldHeavy, playTour, queueUntilSeated, readPosition, } from "./tour.js";
|
|
19
|
+
export { act, enterTour, foldHeavy, playTour, queueUntilSeated, QUEUE_RESTART_BOUND_MS, TourEntryUnavailable, readPosition, } from "./tour.js";
|
|
20
|
+
/* The private room: an agent opens one, another joins it by the id the opener
|
|
21
|
+
hands out, and the guest's seat is taken by accepting the offer the join
|
|
22
|
+
answers -- with its own key, because nothing else holds one. */
|
|
23
|
+
export { joinRoom, MAX_ROOM_SEATS, MIN_ROOM_SEATS, openRoom, roomInvitePrompt, } from "./room.js";
|
|
20
24
|
/* The agent's half of a claim: an invitation its own key signs, carried as a
|
|
21
25
|
token in the claim link its operator hands the owner. */
|
|
22
|
-
export { AGENT_CLAIM_INVITE_DOMAIN, AGENT_CLAIM_INVITE_MAX_WINDOW_MS, AGENT_CLAIM_INVITE_TOKEN_BYTES, claimInviteCanonicalPayload, claimInviteLink, claimInviteSigningBytes, decodeClaimInvite, encodeClaimInvite, mintClaimInvite, } from "./claim.js";
|
|
26
|
+
export { AGENT_CLAIM_INVITE_DOMAIN, AGENT_CLAIM_INVITE_MAX_WINDOW_MS, AGENT_CLAIM_INVITE_TOKEN_BYTES, claimInviteCanonicalPayload, claimInviteLink, claimInviteSigningBytes, decodeClaimInvite, encodeClaimInvite, encodeClaimInviteCompact, mintClaimInvite, } from "./claim.js";
|
|
27
|
+
export { baselineDecision, baselineMove, opponentsStillIn, } from "./decide.js";
|
|
28
|
+
export { handEquity, requiredEquity, cardCode, } from "./equity.js";
|
|
29
|
+
export { bestFive, cardIndex, handCategory, scoreHand, HAND_CATEGORIES, RANK_ORDER, SUIT_ORDER, } from "./handRank.js";
|
|
30
|
+
export { DEFAULT_SEAT_STATE_FILE, loadSeatState, saveSeatState, } from "./seatState.js";
|
|
31
|
+
export { newSeatState, openTurn, submitTurn, } from "./seatTurn.js";
|
|
32
|
+
export { chipAddressOf, joinTransaction, leaveTransaction, listTournaments, planJoin, presentToTournament, readAgentEntry, readPass, readOwner, playsHeldBy, giveBackTransaction, readTournament, sponsorAndExecute, tournamentIdArg, tournamentPossessionSignature, JoinRefused, TournamentRefusal, TOURNAMENT_POSSESSION_SEAT, } from "./openTournament.js";
|
|
33
|
+
export { ClientRefusal, describeNext, refusalLines, refusalMessage, refusalMessageFromText, } from "./refusal.js";
|
package/dist/keypair.js
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* portable and agent-owned. One file carrying both semantics is how one ends
|
|
14
14
|
* up pasted into the other's slot.
|
|
15
15
|
*/
|
|
16
|
-
import { chmodSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { dirname } from "node:path";
|
|
17
18
|
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
|
|
18
19
|
import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
|
|
19
20
|
import { fromHex } from "./bytes.js";
|
|
@@ -38,6 +39,12 @@ export function keypairFromSeed(seed) {
|
|
|
38
39
|
* format is Sui's own bech32, so every ecosystem tool that understands a Sui
|
|
39
40
|
* key understands this file. */
|
|
40
41
|
export function saveKeypair(agent, path = DEFAULT_KEY_FILE) {
|
|
42
|
+
const parent = dirname(path);
|
|
43
|
+
if (parent && parent !== ".") {
|
|
44
|
+
mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
45
|
+
// mkdirSync's mode is umask-masked; the documented key nest is 0700.
|
|
46
|
+
chmodSync(parent, 0o700);
|
|
47
|
+
}
|
|
41
48
|
writeFileSync(path, `${agent.keypair.getSecretKey()}\n`, { mode: 0o600 });
|
|
42
49
|
// mode on writeFileSync only applies at creation; an existing file keeps
|
|
43
50
|
// its bits, so tighten explicitly rather than trusting the happy path
|
package/dist/offer.d.ts
CHANGED
|
@@ -9,6 +9,13 @@ export interface OfferRecord {
|
|
|
9
9
|
state: string;
|
|
10
10
|
seats: OfferSeat[];
|
|
11
11
|
acceptedSeats: number[];
|
|
12
|
+
/** The seats the product filled itself, ascending, empty when none.
|
|
13
|
+
*
|
|
14
|
+
* A house seat signs its acceptance exactly as an agent does, so
|
|
15
|
+
* `acceptedSeats` cannot tell you which opponents asked to be here. Read
|
|
16
|
+
* this before you accept: a hand played against a house seat counts
|
|
17
|
+
* toward no standing, present or future. */
|
|
18
|
+
houseSeats: number[];
|
|
12
19
|
admission?: {
|
|
13
20
|
sessionBaseUrl: string;
|
|
14
21
|
coordinatorPublicKey: string;
|