@block52/poker-vm-sdk 1.8.0 → 1.12.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/dist/index.js CHANGED
@@ -2849,6 +2849,7 @@ function createBaseMsgCreateGame() {
2849
2849
  name: "",
2850
2850
  bettingLimit: "",
2851
2851
  startTime: Long.ZERO,
2852
+ blindStructure: "",
2852
2853
  };
2853
2854
  }
2854
2855
  const MsgCreateGame = {
@@ -2913,6 +2914,9 @@ const MsgCreateGame = {
2913
2914
  if (!message.startTime.equals(Long.ZERO)) {
2914
2915
  writer.uint32(168).int64(message.startTime.toString());
2915
2916
  }
2917
+ if (message.blindStructure !== "") {
2918
+ writer.uint32(178).string(message.blindStructure);
2919
+ }
2916
2920
  return writer;
2917
2921
  },
2918
2922
  decode(input, length) {
@@ -3062,6 +3066,13 @@ const MsgCreateGame = {
3062
3066
  message.startTime = Long.fromString(reader.int64().toString());
3063
3067
  continue;
3064
3068
  }
3069
+ case 22: {
3070
+ if (tag !== 178) {
3071
+ break;
3072
+ }
3073
+ message.blindStructure = reader.string();
3074
+ continue;
3075
+ }
3065
3076
  }
3066
3077
  if ((tag & 7) === 4 || tag === 0) {
3067
3078
  break;
@@ -3092,6 +3103,7 @@ const MsgCreateGame = {
3092
3103
  name: isSet$2b(object.name) ? globalThis.String(object.name) : "",
3093
3104
  bettingLimit: isSet$2b(object.bettingLimit) ? globalThis.String(object.bettingLimit) : "",
3094
3105
  startTime: isSet$2b(object.startTime) ? Long.fromValue(object.startTime) : Long.ZERO,
3106
+ blindStructure: isSet$2b(object.blindStructure) ? globalThis.String(object.blindStructure) : "",
3095
3107
  };
3096
3108
  },
3097
3109
  toJSON(message) {
@@ -3156,6 +3168,9 @@ const MsgCreateGame = {
3156
3168
  if (!message.startTime.equals(Long.ZERO)) {
3157
3169
  obj.startTime = (message.startTime || Long.ZERO).toString();
3158
3170
  }
3171
+ if (message.blindStructure !== "") {
3172
+ obj.blindStructure = message.blindStructure;
3173
+ }
3159
3174
  return obj;
3160
3175
  },
3161
3176
  create(base) {
@@ -3204,6 +3219,7 @@ const MsgCreateGame = {
3204
3219
  message.startTime = (object.startTime !== undefined && object.startTime !== null)
3205
3220
  ? Long.fromValue(object.startTime)
3206
3221
  : Long.ZERO;
3222
+ message.blindStructure = object.blindStructure ?? "";
3207
3223
  message.entryFee = (object.entryFee !== undefined && object.entryFee !== null)
3208
3224
  ? Long.fromValue(object.entryFee)
3209
3225
  : Long.UZERO;
@@ -56115,6 +56131,14 @@ exports.NonPlayerActionType = void 0;
56115
56131
  // starts the sit-and-go once gameOptions.scheduledStartTime has passed and
56116
56132
  // at least minPlayers are seated. Offered in legalActions when it is due.
56117
56133
  NonPlayerActionType["START"] = "start";
56134
+ // Sealed hands (poker-vm#2695, commit-reveal phase 1). Submitted by the
56135
+ // dealer, never by a player. REVEAL_BOARD supplies the next street's cards
56136
+ // (`cards=Xx-Yy-Zz`: 3 for the flop, 1 for the turn, 1 for the river) while
56137
+ // `sealed.awaiting === "board"`. REVEAL_DEAL supplies the full deck
56138
+ // (`deck=<52 cards>`) at showdown while `sealed.awaiting === "deal"`; the
56139
+ // engine replays the deal in `sealed.dealtSeats` order and settles.
56140
+ NonPlayerActionType["REVEAL_BOARD"] = "reveal-board";
56141
+ NonPlayerActionType["REVEAL_DEAL"] = "reveal-deal";
56118
56142
  })(exports.NonPlayerActionType || (exports.NonPlayerActionType = {}));
56119
56143
  const AllPlayerActions = { ...exports.PlayerActionType, ...exports.NonPlayerActionType };
56120
56144
  /**
@@ -56187,6 +56211,8 @@ exports.TexasHoldemRound = void 0;
56187
56211
  TexasHoldemRound["SHOWDOWN"] = "showdown";
56188
56212
  TexasHoldemRound["END"] = "end";
56189
56213
  })(exports.TexasHoldemRound || (exports.TexasHoldemRound = {}));
56214
+ const BLIND_PRESETS = ["turbo", "standard", "deep_stack", "hyper_turbo"];
56215
+ const BLIND_PROGRESSIONS = ["double", "linear", "percentage", "table"];
56190
56216
  const SIT_OUT_METHOD_NEXT_HAND = "next-hand";
56191
56217
  const SIT_OUT_METHOD_NEXT_BB = "next-bb";
56192
56218
  exports.SUIT = void 0;
@@ -56593,6 +56619,451 @@ const isValidPlayerAddress = (address) => {
56593
56619
  return !isEmptyAddress(address);
56594
56620
  };
56595
56621
 
56622
+ /**
56623
+ * The text form of blind structures (poker-vm#1734's "SPL", decided on #2613 /
56624
+ * #2713): a human-readable notation over the typed `BlindStructureDTO`, which
56625
+ * stays canonical in the engine and on chain. These two functions are the only
56626
+ * conversion (Commandment 12); no parser runs inside consensus code.
56627
+ *
56628
+ * BLINDS turbo a preset, no block
56629
+ *
56630
+ * BLINDS custom { algorithmic
56631
+ * start: 25/50
56632
+ * duration: 5m
56633
+ * progression: double | linear 25 | percentage 1.5
56634
+ * round: 25 percentage only (the engine's roundTo)
56635
+ * ante: bb | bb * 0.1 | level >= 4 ? bb * 0.1 : 0
56636
+ * cap: 15
56637
+ * }
56638
+ *
56639
+ * BLINDS wsop_main { table
56640
+ * duration: 120m default for rows without their own
56641
+ * ante: bb rule applied per row unless the row gives an amount
56642
+ * levels {
56643
+ * 0: 100/200
56644
+ * 1: 200/300 ante 300 duration 90m
56645
+ * 2: break 20m
56646
+ * }
56647
+ * }
56648
+ *
56649
+ * Fields may share a line, separated by spaces. `#` starts a comment. Level
56650
+ * numbers are the engine's 0-based levels everywhere (`levels` rows, `cap`,
56651
+ * `level >=`): a table row's position is its level in the engine, so rows run
56652
+ * 0, 1, 2… without gaps. Amounts are chip-unit integers; `ante` multipliers
56653
+ * and `percentage` factors have at most four decimals (basis points).
56654
+ * `parseBlindStructure(formatBlindStructure(x))` deep-equals `x`.
56655
+ */
56656
+ const BPS = 10000n;
56657
+ const IDENT = /^[A-Za-z_][\w-]*$/;
56658
+ const FIELD_KEY = /^([A-Za-z_]\w*):$/;
56659
+ const ROW_KEY = /^(\d+):$/;
56660
+ function fail(message) {
56661
+ throw new Error(`blind structure: ${message}`);
56662
+ }
56663
+ function tokenize(text) {
56664
+ const noComments = text
56665
+ .split("\n")
56666
+ .map(line => line.replace(/#.*$/, ""))
56667
+ .join("\n");
56668
+ return noComments
56669
+ .replace(/\{/g, " { ")
56670
+ .replace(/\}/g, " } ")
56671
+ .split(/\s+/)
56672
+ .filter(t => t.length > 0);
56673
+ }
56674
+ function parseAmount(what, s) {
56675
+ if (!/^\d+$/.test(s))
56676
+ fail(`${what} must be a whole number of chips, got "${s}"`);
56677
+ return BigInt(s).toString();
56678
+ }
56679
+ function parseMinutes(what, s) {
56680
+ const m = /^(\d+)(m|h)$/.exec(s);
56681
+ if (!m)
56682
+ fail(`${what} must be a duration like 5m or 2h, got "${s}"`);
56683
+ const n = Number(m[1]) * (m[2] === "h" ? 60 : 1);
56684
+ if (n <= 0)
56685
+ fail(`${what} must be positive`);
56686
+ return n;
56687
+ }
56688
+ function parseInteger(what, s) {
56689
+ if (!/^\d+$/.test(s) || !Number.isSafeInteger(Number(s)))
56690
+ fail(`${what} must be a whole number, got "${s}"`);
56691
+ return Number(s);
56692
+ }
56693
+ /** "1.5" → 15000, "0.125" → 1250 (exact; at most four decimals). */
56694
+ function parseBps(what, s) {
56695
+ const m = /^(\d+)(?:\.(\d{1,4}))?$/.exec(s);
56696
+ if (!m)
56697
+ fail(`${what} must be a decimal with at most four places, got "${s}"`);
56698
+ return Number(m[1]) * 10000 + Number((m[2] ?? "").padEnd(4, "0"));
56699
+ }
56700
+ function formatBps(bps) {
56701
+ const whole = Math.floor(bps / 10000);
56702
+ const frac = String(bps % 10000).padStart(4, "0").replace(/0+$/, "");
56703
+ return frac === "" ? String(whole) : `${whole}.${frac}`;
56704
+ }
56705
+ function parseAnteRule(expr) {
56706
+ const text = expr.replace(/\s+/g, " ").trim();
56707
+ if (text === "0")
56708
+ return undefined;
56709
+ const conditional = /^level >= (\d+) \? (.+) : 0$/.exec(text);
56710
+ const startLevel = conditional ? parseInteger("ante start level", conditional[1]) : 0;
56711
+ const multiplier = (conditional ? conditional[2] : text).replace(/\s+/g, "");
56712
+ if (multiplier === "bb")
56713
+ return { startLevel, bps: 10000 };
56714
+ const m = /^bb\*(\d+(?:\.\d+)?)$/.exec(multiplier);
56715
+ if (!m)
56716
+ fail(`ante must be "bb", "bb * <fraction>" or "level >= N ? bb * <fraction> : 0", got "${expr}"`);
56717
+ const bps = parseBps("ante fraction", m[1]);
56718
+ if (bps <= 0 || bps > 10000)
56719
+ fail(`ante fraction must be within 0..1, got "${m[1]}"`);
56720
+ return { startLevel, bps };
56721
+ }
56722
+ function formatAnteRule(rule) {
56723
+ const multiplier = rule.bps === 10000 ? "bb" : `bb * ${formatBps(rule.bps)}`;
56724
+ return rule.startLevel > 0 ? `level >= ${rule.startLevel} ? ${multiplier} : 0` : multiplier;
56725
+ }
56726
+ function anteFor(bigBlind, rule, level) {
56727
+ if (!rule || level < rule.startLevel)
56728
+ return "0";
56729
+ return ((BigInt(bigBlind) * BigInt(rule.bps)) / BPS).toString();
56730
+ }
56731
+ /** Splits the block body into `key:` fields (value = the tokens up to the next key) and the `levels { … }` rows. */
56732
+ function readBody(toks) {
56733
+ const fields = new Map();
56734
+ let rows;
56735
+ let i = 0;
56736
+ while (i < toks.length) {
56737
+ const tok = toks[i];
56738
+ if (tok === "levels") {
56739
+ if (toks[i + 1] !== "{")
56740
+ fail("levels must be followed by { … }");
56741
+ if (rows)
56742
+ fail("levels given twice");
56743
+ rows = new Map();
56744
+ i += 2;
56745
+ let current;
56746
+ for (; i < toks.length && toks[i] !== "}"; i++) {
56747
+ const row = ROW_KEY.exec(toks[i]);
56748
+ if (row) {
56749
+ current = Number(row[1]);
56750
+ if (rows.has(current))
56751
+ fail(`level ${current} given twice`);
56752
+ rows.set(current, []);
56753
+ }
56754
+ else if (current === undefined) {
56755
+ fail(`expected a level number like "0:" before "${toks[i]}"`);
56756
+ }
56757
+ else {
56758
+ rows.get(current).push(toks[i]);
56759
+ }
56760
+ }
56761
+ if (i >= toks.length)
56762
+ fail("levels block is not closed");
56763
+ i++;
56764
+ continue;
56765
+ }
56766
+ const key = FIELD_KEY.exec(tok);
56767
+ if (!key)
56768
+ fail(`expected a field like "start:" but found "${tok}"`);
56769
+ if (fields.has(key[1]))
56770
+ fail(`${key[1]} given twice`);
56771
+ const value = [];
56772
+ for (i++; i < toks.length && !FIELD_KEY.test(toks[i]) && toks[i] !== "levels"; i++)
56773
+ value.push(toks[i]);
56774
+ if (value.length === 0)
56775
+ fail(`${key[1]} has no value`);
56776
+ fields.set(key[1], value);
56777
+ }
56778
+ return { fields, rows };
56779
+ }
56780
+ const ALGORITHMIC_FIELDS = new Set(["start", "duration", "progression", "round", "ante", "cap"]);
56781
+ const TABLE_FIELDS = new Set(["duration", "ante", "cap"]);
56782
+ function parseRow(level, toks, defaults) {
56783
+ if (toks[0] === "break") {
56784
+ if (toks.length !== 2)
56785
+ fail(`level ${level}: a break is "break <duration>"`);
56786
+ return { level, smallBlind: "", bigBlind: "", ante: "", durationMinutes: parseMinutes(`level ${level} break`, toks[1]), isBreak: true };
56787
+ }
56788
+ const blinds = /^(\d+)\/(\d+)$/.exec(toks[0] ?? "");
56789
+ if (!blinds)
56790
+ fail(`level ${level}: expected "<sb>/<bb>" or "break <duration>", got "${toks.join(" ")}"`);
56791
+ const smallBlind = parseAmount(`level ${level} small blind`, blinds[1]);
56792
+ const bigBlind = parseAmount(`level ${level} big blind`, blinds[2]);
56793
+ if (BigInt(smallBlind) <= 0n || BigInt(bigBlind) < BigInt(smallBlind)) {
56794
+ fail(`level ${level}: blinds must be positive with the big blind at least the small blind`);
56795
+ }
56796
+ let ante;
56797
+ let durationMinutes;
56798
+ for (let i = 1; i < toks.length; i += 2) {
56799
+ const value = toks[i + 1];
56800
+ if (value === undefined)
56801
+ fail(`level ${level}: "${toks[i]}" needs a value`);
56802
+ if (toks[i] === "ante") {
56803
+ if (ante !== undefined)
56804
+ fail(`level ${level}: ante given twice`);
56805
+ ante = parseAmount(`level ${level} ante`, value);
56806
+ }
56807
+ else if (toks[i] === "duration") {
56808
+ if (durationMinutes !== undefined)
56809
+ fail(`level ${level}: duration given twice`);
56810
+ durationMinutes = parseMinutes(`level ${level} duration`, value);
56811
+ }
56812
+ else {
56813
+ fail(`level ${level}: unknown row option "${toks[i]}" (ante or duration)`);
56814
+ }
56815
+ }
56816
+ if (durationMinutes === undefined)
56817
+ durationMinutes = defaults.duration;
56818
+ if (durationMinutes === undefined)
56819
+ fail(`level ${level} has no duration and the structure sets no default "duration:"`);
56820
+ return { level, smallBlind, bigBlind, ante: ante ?? anteFor(bigBlind, defaults.ante, level), durationMinutes, isBreak: false };
56821
+ }
56822
+ function parseTable(body) {
56823
+ for (const key of body.fields.keys()) {
56824
+ if (!TABLE_FIELDS.has(key))
56825
+ fail(`"${key}" is not a table-mode field (a table takes its blinds from levels)`);
56826
+ }
56827
+ const defaults = {
56828
+ duration: body.fields.has("duration") ? parseMinutes("duration", body.fields.get("duration").join(" ")) : undefined,
56829
+ ante: body.fields.has("ante") ? parseAnteRule(body.fields.get("ante").join(" ")) : undefined
56830
+ };
56831
+ const rows = body.rows;
56832
+ if (rows.size === 0)
56833
+ fail("levels has no rows");
56834
+ const levels = [];
56835
+ const numbers = [...rows.keys()];
56836
+ numbers.forEach((n, i) => {
56837
+ if (n !== i)
56838
+ fail(`levels must run 0, 1, 2… in order (0-based, as the engine numbers them); found level ${n} at position ${i}`);
56839
+ levels.push(parseRow(n, rows.get(n), defaults));
56840
+ });
56841
+ if (levels[0].isBreak)
56842
+ fail("the first level must not be a break");
56843
+ if (levels[levels.length - 1].isBreak)
56844
+ fail("the last level must not be a break (it repeats once the schedule runs out)");
56845
+ const out = { progression: "table", levels };
56846
+ if (body.fields.has("cap"))
56847
+ out.maxLevel = parseInteger("cap", body.fields.get("cap").join(" "));
56848
+ return out;
56849
+ }
56850
+ function parseAlgorithmic(body) {
56851
+ const f = body.fields;
56852
+ for (const key of f.keys()) {
56853
+ if (!ALGORITHMIC_FIELDS.has(key))
56854
+ fail(`unknown field "${key}" (start, duration, progression, round, ante, cap, levels)`);
56855
+ }
56856
+ for (const required of ["start", "duration", "progression"]) {
56857
+ if (!f.has(required))
56858
+ fail(`${required} is required (or give levels { … } for a table)`);
56859
+ }
56860
+ const start = /^(\d+)\/(\d+)$/.exec(f.get("start").join(""));
56861
+ if (!start)
56862
+ fail(`start must be "<sb>/<bb>", got "${f.get("start").join(" ")}"`);
56863
+ const startSmallBlind = parseAmount("start small blind", start[1]);
56864
+ const startBigBlind = parseAmount("start big blind", start[2]);
56865
+ if (BigInt(startSmallBlind) <= 0n || BigInt(startBigBlind) !== 2n * BigInt(startSmallBlind)) {
56866
+ fail(`opening blinds ${startSmallBlind}/${startBigBlind}: the algorithmic progressions need a positive small blind and big blind = 2 × small blind`);
56867
+ }
56868
+ const base = {
56869
+ startSmallBlind,
56870
+ startBigBlind,
56871
+ durationMinutes: parseMinutes("duration", f.get("duration").join(" ")),
56872
+ ...(f.has("cap") ? { maxLevel: parseInteger("cap", f.get("cap").join(" ")) } : {}),
56873
+ ...(f.has("ante") ? (() => { const ante = parseAnteRule(f.get("ante").join(" ")); return ante ? { ante } : {}; })() : {})
56874
+ };
56875
+ const [kind, ...rest] = f.get("progression");
56876
+ switch (kind) {
56877
+ case "double":
56878
+ if (rest.length !== 0 || f.has("round"))
56879
+ fail("double takes no argument and no round");
56880
+ return { progression: "double", ...base };
56881
+ case "linear": {
56882
+ if (rest.length !== 1 || f.has("round"))
56883
+ fail('linear needs one step, e.g. "linear 25", and no round');
56884
+ const step = parseAmount("linear step", rest[0]);
56885
+ if (BigInt(step) <= 0n)
56886
+ fail("linear step must be positive");
56887
+ return { progression: "linear", ...base, step };
56888
+ }
56889
+ case "percentage": {
56890
+ if (rest.length !== 1)
56891
+ fail('percentage needs one factor, e.g. "percentage 1.5"');
56892
+ const factorBps = parseBps("percentage factor", rest[0]);
56893
+ if (factorBps <= 10000)
56894
+ fail(`percentage factor must exceed 1, got "${rest[0]}"`);
56895
+ if (!f.has("round"))
56896
+ fail('percentage needs "round: <chips>" (the engine rounds each level to a multiple of it)');
56897
+ const roundTo = parseAmount("round", f.get("round").join(" "));
56898
+ if (BigInt(roundTo) <= 0n)
56899
+ fail("round must be positive");
56900
+ return { progression: "percentage", ...base, factorBps, roundTo };
56901
+ }
56902
+ default:
56903
+ fail(`progression must be double, "linear <step>" or "percentage <factor>", got "${[kind, ...rest].join(" ")}"`);
56904
+ }
56905
+ }
56906
+ /**
56907
+ * Parses the text form into the typed structure. Throws on anything the
56908
+ * engine would refuse that can be seen without expanding levels; the engine
56909
+ * validates the result again on load.
56910
+ */
56911
+ function parseBlindStructure(text) {
56912
+ const toks = tokenize(text);
56913
+ if (toks[0] !== "BLINDS")
56914
+ fail('must start with "BLINDS <name>"');
56915
+ const name = toks[1];
56916
+ if (name === undefined || !IDENT.test(name))
56917
+ fail(`"BLINDS" needs a name, got "${name ?? ""}"`);
56918
+ if (toks.length === 2) {
56919
+ if (!BLIND_PRESETS.includes(name)) {
56920
+ fail(`"${name}" is not a preset (${BLIND_PRESETS.join(", ")}); a custom structure needs a { … } block`);
56921
+ }
56922
+ return { preset: name };
56923
+ }
56924
+ if (toks[2] !== "{" || toks[toks.length - 1] !== "}")
56925
+ fail("the structure must be a { … } block after the name");
56926
+ if (BLIND_PRESETS.includes(name))
56927
+ fail(`"${name}" is a preset and takes no block`);
56928
+ const body = readBody(toks.slice(3, -1));
56929
+ return body.rows ? parseTable(body) : parseAlgorithmic(body);
56930
+ }
56931
+ /**
56932
+ * The inverse of parseBlindStructure. A preset formats as `BLINDS <preset>`;
56933
+ * anything else as a block under `name` (default "custom").
56934
+ */
56935
+ function formatBlindStructure(structure, name = "custom") {
56936
+ if ("preset" in structure)
56937
+ return `BLINDS ${structure.preset}`;
56938
+ if (!IDENT.test(name) || BLIND_PRESETS.includes(name))
56939
+ fail(`"${name}" is not a usable structure name`);
56940
+ const lines = [];
56941
+ if (structure.progression === "table") {
56942
+ lines.push(" levels {");
56943
+ for (const l of structure.levels) {
56944
+ if (l.isBreak) {
56945
+ lines.push(` ${l.level}: break ${l.durationMinutes}m`);
56946
+ }
56947
+ else {
56948
+ const ante = l.ante !== "0" ? ` ante ${l.ante}` : "";
56949
+ lines.push(` ${l.level}: ${l.smallBlind}/${l.bigBlind}${ante} duration ${l.durationMinutes}m`);
56950
+ }
56951
+ }
56952
+ lines.push(" }");
56953
+ }
56954
+ else {
56955
+ lines.push(` start: ${structure.startSmallBlind}/${structure.startBigBlind}`);
56956
+ lines.push(` duration: ${structure.durationMinutes}m`);
56957
+ switch (structure.progression) {
56958
+ case "double":
56959
+ lines.push(" progression: double");
56960
+ break;
56961
+ case "linear":
56962
+ lines.push(` progression: linear ${structure.step}`);
56963
+ break;
56964
+ case "percentage":
56965
+ lines.push(` progression: percentage ${formatBps(structure.factorBps)}`);
56966
+ lines.push(` round: ${structure.roundTo}`);
56967
+ break;
56968
+ }
56969
+ if (structure.ante)
56970
+ lines.push(` ante: ${formatAnteRule(structure.ante)}`);
56971
+ }
56972
+ if (structure.maxLevel !== undefined)
56973
+ lines.push(` cap: ${structure.maxLevel}`);
56974
+ return [`BLINDS ${name} {`, ...lines, "}"].join("\n");
56975
+ }
56976
+ // --- Level expansion (poker-vm#2718) --------------------------------------
56977
+ //
56978
+ // A TypeScript port of the engine's ExpandLevel (pvm/go/engine/tournament_blinds.go),
56979
+ // integer-exact with BigInt, so a create-form preview or a table's "Blinds"
56980
+ // panel shows exactly the levels the engine will play. Pinned against the
56981
+ // engine's own expansions in blindStructure.test.ts.
56982
+ /**
56983
+ * #1734's preset library, as the engine resolves it (pvm/go/engine
56984
+ * tournament_blinds.go blindPresets). The engine resolves a preset on the
56985
+ * table's first action; `resolveBlindPreset` does the same client-side so the
56986
+ * levels can be previewed before the table exists.
56987
+ */
56988
+ const BLIND_PRESET_STRUCTURES = {
56989
+ turbo: { progression: "double", startSmallBlind: "25", startBigBlind: "50", durationMinutes: 5, ante: { startLevel: 4, bps: 1000 } },
56990
+ standard: { progression: "double", startSmallBlind: "25", startBigBlind: "50", durationMinutes: 10, ante: { startLevel: 5, bps: 1000 } },
56991
+ deep_stack: {
56992
+ progression: "percentage",
56993
+ startSmallBlind: "25",
56994
+ startBigBlind: "50",
56995
+ factorBps: 15000,
56996
+ roundTo: "25",
56997
+ durationMinutes: 15,
56998
+ ante: { startLevel: 6, bps: 1250 }
56999
+ },
57000
+ hyper_turbo: { progression: "double", startSmallBlind: "50", startBigBlind: "100", durationMinutes: 3, ante: { startLevel: 3, bps: 1000 } }
57001
+ };
57002
+ /** Replaces a preset with its concrete structure (a deep copy); anything else is returned as is. */
57003
+ function resolveBlindPreset(structure) {
57004
+ if (!("preset" in structure))
57005
+ return structure;
57006
+ const preset = BLIND_PRESET_STRUCTURES[structure.preset];
57007
+ if (!preset)
57008
+ fail(`"${structure.preset}" is not a preset (${BLIND_PRESETS.join(", ")})`);
57009
+ return JSON.parse(JSON.stringify(preset));
57010
+ }
57011
+ function algorithmicSmallBlind(structure, n) {
57012
+ const sb0 = BigInt(structure.startSmallBlind);
57013
+ switch (structure.progression) {
57014
+ case "double":
57015
+ return sb0 << BigInt(n);
57016
+ case "linear":
57017
+ return sb0 + BigInt(structure.step) * BigInt(n);
57018
+ case "percentage": {
57019
+ const roundTo = BigInt(structure.roundTo);
57020
+ const factor = BigInt(structure.factorBps);
57021
+ let sb = sb0;
57022
+ for (let i = 0; i < n; i++) {
57023
+ const raw = (sb * factor) / BPS;
57024
+ let q = raw / roundTo;
57025
+ const r = raw % roundTo;
57026
+ if (r * 2n >= roundTo)
57027
+ q += 1n;
57028
+ let rounded = q * roundTo;
57029
+ const floor = sb + roundTo;
57030
+ if (rounded < floor)
57031
+ rounded = floor;
57032
+ sb = rounded;
57033
+ }
57034
+ return sb;
57035
+ }
57036
+ }
57037
+ }
57038
+ /**
57039
+ * Level n (0-based) of a structure, exactly as the engine expands it: the cap
57040
+ * (`maxLevel`) freezes the blinds while the level number keeps counting; a
57041
+ * table schedule repeats its last row once it runs out; the ante rule applies
57042
+ * from its start level. A preset must be resolved first (`resolveBlindPreset`).
57043
+ */
57044
+ function expandBlindLevel(structure, n) {
57045
+ if (!Number.isInteger(n) || n < 0)
57046
+ fail(`blind level ${n} must be a non-negative integer`);
57047
+ if ("preset" in structure)
57048
+ fail(`resolve the "${structure.preset}" preset before expanding it (resolveBlindPreset)`);
57049
+ const m = structure.maxLevel !== undefined && n > structure.maxLevel ? structure.maxLevel : n;
57050
+ if (structure.progression === "table") {
57051
+ const rows = structure.levels;
57052
+ if (rows.length === 0)
57053
+ fail("a table structure needs at least one level");
57054
+ const row = { ...rows[Math.min(m, rows.length - 1)] };
57055
+ row.level = n;
57056
+ row.durationMinutes = n >= rows.length ? rows[rows.length - 1].durationMinutes : rows[n].durationMinutes;
57057
+ return row;
57058
+ }
57059
+ const sb = algorithmicSmallBlind(structure, m);
57060
+ const bb = sb * 2n;
57061
+ let ante = 0n;
57062
+ if (structure.ante && n >= structure.ante.startLevel)
57063
+ ante = (bb * BigInt(structure.ante.bps)) / BPS;
57064
+ return { level: n, smallBlind: sb.toString(), bigBlind: bb.toString(), ante: ante.toString(), durationMinutes: structure.durationMinutes, isBreak: false };
57065
+ }
57066
+
56596
57067
  /**
56597
57068
  * Poker hand types in order of strength (weakest to strongest)
56598
57069
  */
@@ -71176,8 +71647,8 @@ class SigningCosmosClient extends CosmosClient {
71176
71647
  if (sngConfig.startingStack <= 0n) {
71177
71648
  throw new Error(`${gameFormat} games require a positive startingStack; got ${sngConfig.startingStack}.`);
71178
71649
  }
71179
- if (!sngConfig.blindLevelDuration || sngConfig.blindLevelDuration <= 0) {
71180
- throw new Error(`${gameFormat} games require a positive blindLevelDuration; got ${sngConfig.blindLevelDuration}.`);
71650
+ if (!sngConfig.blindStructure && (!sngConfig.blindLevelDuration || sngConfig.blindLevelDuration <= 0)) {
71651
+ throw new Error(`${gameFormat} games require a positive blindLevelDuration (or a blindStructure); got ${sngConfig.blindLevelDuration}.`);
71181
71652
  }
71182
71653
  assertCreatableSngField(minPlayers, maxPlayers, sngConfig);
71183
71654
  }
@@ -71220,6 +71691,9 @@ class SigningCosmosClient extends CosmosClient {
71220
71691
  // Scheduled start (poker-vm#2680), unix seconds. 0 = unscheduled, and
71221
71692
  // a zero proto3 int64 is not encoded at all.
71222
71693
  startTime: sngConfig?.startTime ? Long.fromNumber(sngConfig.startTime) : Long.ZERO,
71694
+ // Blind structure (block52/pokerchain#434) as JSON; "" = doubling, and
71695
+ // an empty proto3 string is not encoded at all.
71696
+ blindStructure: sngConfig?.blindStructure ? JSON.stringify(sngConfig.blindStructure) : "",
71223
71697
  });
71224
71698
  // Create the transaction message
71225
71699
  const msg = {
@@ -71249,7 +71723,8 @@ class SigningCosmosClient extends CosmosClient {
71249
71723
  ...(sngConfig && {
71250
71724
  startingStack: sngConfig.startingStack.toString(),
71251
71725
  blindLevelDuration: sngConfig.blindLevelDuration,
71252
- entryFee: sngConfig.entryFee?.toString() ?? "0"
71726
+ entryFee: sngConfig.entryFee?.toString() ?? "0",
71727
+ ...(sngConfig.blindStructure && { blindStructure: sngConfig.blindStructure })
71253
71728
  })
71254
71729
  });
71255
71730
  try {
@@ -72171,6 +72646,9 @@ const registry = new protoSigning.Registry([
72171
72646
  ]);
72172
72647
 
72173
72648
  exports.AllPlayerActions = AllPlayerActions;
72649
+ exports.BLIND_PRESETS = BLIND_PRESETS;
72650
+ exports.BLIND_PRESET_STRUCTURES = BLIND_PRESET_STRUCTURES;
72651
+ exports.BLIND_PROGRESSIONS = BLIND_PROGRESSIONS;
72174
72652
  exports.BLOCK52_HD_PATH = BLOCK52_HD_PATH;
72175
72653
  exports.Board = Board;
72176
72654
  exports.COSMOS_CONSTANTS = COSMOS_CONSTANTS;
@@ -72200,6 +72678,8 @@ exports.computeGameNameFee = computeGameNameFee;
72200
72678
  exports.createSigningClientFromMnemonic = createSigningClientFromMnemonic;
72201
72679
  exports.createWalletFromMnemonic = createWalletFromMnemonic;
72202
72680
  exports.decToAtomics = decToAtomics;
72681
+ exports.expandBlindLevel = expandBlindLevel;
72682
+ exports.formatBlindStructure = formatBlindStructure;
72203
72683
  exports.formatGameFormatDisplay = formatGameFormatDisplay;
72204
72684
  exports.formatGameVariantDisplay = formatGameVariantDisplay;
72205
72685
  exports.generateWallet = generateWallet;
@@ -72220,9 +72700,11 @@ exports.isSitAndGoFormat = isSitAndGoFormat;
72220
72700
  exports.isTournamentFormat = isTournamentFormat;
72221
72701
  exports.isValidMnemonic = isValidMnemonic;
72222
72702
  exports.isValidPlayerAddress = isValidPlayerAddress;
72703
+ exports.parseBlindStructure = parseBlindStructure;
72223
72704
  exports.parseGameFormat = parseGameFormat;
72224
72705
  exports.parseGameVariant = parseGameVariant;
72225
72706
  exports.registry = registry;
72707
+ exports.resolveBlindPreset = resolveBlindPreset;
72226
72708
  exports.toValoperAddress = toValoperAddress;
72227
72709
  exports.txFailureHash = txFailureHash;
72228
72710
  exports.txFailurePhase = txFailurePhase;