texas-poker-core 1.4.26 → 1.4.28

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 CHANGED
@@ -608,4 +608,12 @@ fix: 修复结算金额分配异常
608
608
  - 导出 `createStandardDeckPokes`,生成标准 52 张牌(与 `Deck` 建牌顺序一致)
609
609
 
610
610
  ## 1.4.26
611
- export new functiono
611
+
612
+ - 版本发布
613
+
614
+ ## 1.4.27
615
+
616
+ reject modulo bias
617
+
618
+ ## 1.4.28
619
+ 贴盲与game start原子化
@@ -11,6 +11,7 @@ Object.defineProperty(exports, "StageEnum", {
11
11
  }
12
12
  });
13
13
  exports.default = void 0;
14
+ var _Player = require("../Player");
14
15
  var _CurrentHand = require("../Hand/CurrentHand");
15
16
  var _handBettingActions = require("../Player/handBettingActions");
16
17
  var _TexasEngineContext = require("../TexasEngineContext");
@@ -22,6 +23,7 @@ function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLim
22
23
  function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
23
24
  function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
24
25
  function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
26
+ function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
25
27
  function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
26
28
  function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
27
29
  function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
@@ -465,77 +467,19 @@ var Controller = /*#__PURE__*/function () {
465
467
  * 入池额 `min(桌大盲, 余额)`,与开局盲注路径一致;**不**调用 `completeBettingTurn`、不改变当前思考权。
466
468
  */
467
469
  function postBigBlindForJoiningPlayer(player) {
468
- if (_classPrivateFieldGet(_hand, this).status !== 'in_hand') {
469
- return this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.CTRL_POST_BB_NOT_IN_HAND));
470
- }
471
- if (_classPrivateFieldGet(_hand, this).stage !== _stage.StageEnum.PRE_FLOP) {
472
- return this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.CTRL_POST_BB_NOT_PREFLOP));
473
- }
474
- if (_classPrivateFieldGet(_hand, this).activePlayer === player) {
475
- return this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.CTRL_POST_BB_IS_ACTIVE_PLAYER));
476
- }
477
- if (player.getStatus() !== 'eligible') {
478
- return this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.CTRL_POST_BB_PLAYER_INELIGIBLE));
479
- }
480
- if (player.currentStageTotalAmount !== 0) {
481
- return this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.CTRL_POST_BB_ALREADY_CONTRIBUTED));
482
- }
483
- var requested = _classPrivateFieldGet(_dealer, this).stakes.bigBlind;
484
- var balanceBefore = player.balance;
485
- if (balanceBefore <= 0) {
486
- return this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.CTRL_POST_BB_NO_CHIPS));
487
- }
488
-
489
- // `requested > 0`(TableStakes)且余额已正 ⇒ `#postBlind` 入池额必为正
490
- var posted = _assertClassBrand(_Controller_brand, this, _postBlind).call(this, player, requested);
470
+ var row = _assertClassBrand(_Controller_brand, this, _applyJoiningBigBlindChips).call(this, player);
491
471
  _classPrivateFieldGet(_handEvents, this).push({
492
- type: 'PostedBigBlind',
472
+ type: 'PostedJoiningBigBlinds',
493
473
  payload: _objectSpread(_objectSpread({}, _assertClassBrand(_Controller_brand, this, _eventMeta).call(this)), {}, {
494
- userId: player.getUserInfo().id,
495
- amount: posted,
496
- requested: requested
474
+ posts: [row]
497
475
  })
498
476
  });
499
- this.recordPotUpdated();
500
477
  }
501
478
  }, {
502
479
  key: "takeActionInPreFlop",
503
480
  value: function takeActionInPreFlop() {
504
- var _this2 = this;
505
- _classPrivateFieldGet(_handEvents, this).push({
506
- type: 'HandStarted',
507
- payload: _assertClassBrand(_Controller_brand, this, _eventMeta).call(this)
508
- });
509
- var takeDefaultActionPlayers = _assertClassBrand(_Controller_brand, this, _getSmallBindAndBigBind).call(this);
510
- var posts = [];
511
- takeDefaultActionPlayers.forEach(function (player, index) {
512
- if (player) {
513
- var requested = index === 0 ? _classPrivateFieldGet(_dealer, _this2).stakes.smallBlind : _classPrivateFieldGet(_dealer, _this2).stakes.bigBlind;
514
- var kind = index === 0 ? 'sb' : 'bb';
515
- var posted = _assertClassBrand(_Controller_brand, _this2, _postBlind).call(_this2, player, requested);
516
- posts.push({
517
- userId: player.getUserInfo().id,
518
- amount: posted,
519
- kind: kind
520
- });
521
- // 细粒度 PotUpdated:与自愿下注一致,每次入池一条(盲注用 skipDomainEvents,此处补发)。
522
- _this2.recordPotUpdated();
523
- }
524
- });
525
- _classPrivateFieldGet(_handEvents, this).push({
526
- type: 'BlindsPosted',
527
- payload: _objectSpread(_objectSpread({}, _assertClassBrand(_Controller_brand, this, _eventMeta).call(this)), {}, {
528
- posts: posts
529
- })
530
- });
531
- var _takeDefaultActionPla = _slicedToArray(takeDefaultActionPlayers, 2),
532
- bigBlind = _takeDefaultActionPla[1];
533
- var activePlayer = bigBlind === null || bigBlind === void 0 ? void 0 : bigBlind.getNextPlayer();
534
- if (activePlayer) {
535
- this.transferControlTo(activePlayer);
536
- } else {
537
- return this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.CTRL_START_NO_ACTIVE));
538
- }
481
+ _assertClassBrand(_Controller_brand, this, _emitHandStarted).call(this);
482
+ _assertClassBrand(_Controller_brand, this, _postTableBlindsAndFirstPreflopActor).call(this);
539
483
  }
540
484
  }, {
541
485
  key: "start",
@@ -545,20 +489,47 @@ var Controller = /*#__PURE__*/function () {
545
489
  * 写入 `HandStarted`、贴盲与 **`pendingFlowOps` / 跑马路状态**,再 `transferControlTo`(首人思考权先入队,待业务 flush)。
546
490
  */
547
491
  function start() {
548
- if (_classPrivateFieldGet(_activeHandId, this) === null) {
549
- return this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.CTRL_START_NO_HAND_PREP));
550
- }
551
- _classPrivateFieldSet(_pendingTurnEndedReason, this, null);
552
- _classPrivateFieldSet(_pendingFlowOps, this, []);
553
- _classPrivateFieldSet(_pausedActiveUserId, this, null);
554
- _classPrivateFieldSet(_runoutMode, this, false);
555
- _classPrivateFieldGet(_hand, this).status = 'in_hand';
556
- _classPrivateFieldGet(_hand, this).stage = _stage.StageEnum.PRE_FLOP;
557
- if (_TexasEngineContext.TexasEngineContext.simulation().resetDealerBeforeHandStart) {
558
- _classPrivateFieldGet(_dealer, this).reset();
559
- }
492
+ _assertClassBrand(_Controller_brand, this, _enterPreflopHandFrame).call(this);
560
493
  this.takeActionInPreFlop();
561
494
  }
495
+
496
+ /**
497
+ * 与 {@link start} 同属一手开局原子路径:先进入翻前帧并 `HandStarted`,再对 `joiningBigBlindUserIds`
498
+ * 依次入账(跳过 SB/BB、环上不存在 id 忽略),其间每条入池后各一条 `PotUpdated`;
499
+ * 最后 **恒** 缓冲一条 {@link PostedJoiningBigBlinds},`posts` 为本次所有入座大盲(无人须贴时为空数组);
500
+ * 再贴桌盲并 `transferControlTo(BB.getNext())`。
501
+ * 业务应仅此入口处理「待贴入座大盲」,勿在 `start()` 后再穿插 `PostBigBlind`。
502
+ */
503
+ }, {
504
+ key: "startPreflopWithJoiningBigBlinds",
505
+ value: function startPreflopWithJoiningBigBlinds(joiningBigBlindUserIds) {
506
+ _assertClassBrand(_Controller_brand, this, _enterPreflopHandFrame).call(this);
507
+ _assertClassBrand(_Controller_brand, this, _emitHandStarted).call(this);
508
+ var joiningPosts = [];
509
+ var _iterator = _createForOfIteratorHelper(joiningBigBlindUserIds),
510
+ _step;
511
+ try {
512
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
513
+ var userId = _step.value;
514
+ var pl = _classPrivateFieldGet(_dealer, this).getById(userId);
515
+ if (!pl) continue;
516
+ var role = pl.getRole();
517
+ if (role === _Player.RoleEnum.SB || role === _Player.RoleEnum.BB) continue;
518
+ joiningPosts.push(_assertClassBrand(_Controller_brand, this, _applyJoiningBigBlindChips).call(this, pl));
519
+ }
520
+ } catch (err) {
521
+ _iterator.e(err);
522
+ } finally {
523
+ _iterator.f();
524
+ }
525
+ _classPrivateFieldGet(_handEvents, this).push({
526
+ type: 'PostedJoiningBigBlinds',
527
+ payload: _objectSpread(_objectSpread({}, _assertClassBrand(_Controller_brand, this, _eventMeta).call(this)), {}, {
528
+ posts: joiningPosts
529
+ })
530
+ });
531
+ _assertClassBrand(_Controller_brand, this, _postTableBlindsAndFirstPreflopActor).call(this);
532
+ }
562
533
  }, {
563
534
  key: "continue",
564
535
  value: function _continue() {
@@ -690,12 +661,12 @@ function _getPokeEndIndex(stage) {
690
661
  if (stage === _stage.StageEnum.RIVER) return 5;
691
662
  }
692
663
  function _performBettingRoundStageAdvance() {
693
- var _this3 = this;
664
+ var _this2 = this;
694
665
  if (!_assertClassBrand(_Controller_brand, this, _everyPlayerNonActionableForRound).call(this)) {
695
666
  return this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.CTRL_FLOW_PENDING_MISMATCH));
696
667
  }
697
668
  var index = _stage.STAGE_ORDER.findIndex(function (stage) {
698
- return stage === _classPrivateFieldGet(_hand, _this3).stage;
669
+ return stage === _classPrivateFieldGet(_hand, _this2).stage;
699
670
  });
700
671
  if (index < 0 || index >= _stage.STAGE_ORDER.length - 1) {
701
672
  return this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.CTRL_FLOW_PENDING_MISMATCH));
@@ -723,9 +694,9 @@ function _performBettingRoundStageAdvance() {
723
694
  * 摊牌跑马路一步:推进 `stage` 并 `StageAdvanced(runout_reveal)`;到河牌则 settle、`HandEnded`。
724
695
  */
725
696
  function _applyOneRunoutRevealStep() {
726
- var _this4 = this;
697
+ var _this3 = this;
727
698
  var index = _stage.STAGE_ORDER.findIndex(function (s) {
728
- return s === _classPrivateFieldGet(_hand, _this4).stage;
699
+ return s === _classPrivateFieldGet(_hand, _this3).stage;
729
700
  });
730
701
  if (index < 0 || index >= _stage.STAGE_ORDER.length - 1) {
731
702
  return this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.CTRL_FLOW_PENDING_MISMATCH));
@@ -760,6 +731,78 @@ function _postBlind(player, requested) {
760
731
  });
761
732
  return posted;
762
733
  }
734
+ /**
735
+ * 入座大盲入账与池快照;供单条指令与批量开局共用(领域事件由调用方缓冲 `PostedJoiningBigBlinds`)。
736
+ */
737
+ function _applyJoiningBigBlindChips(player) {
738
+ if (_classPrivateFieldGet(_hand, this).status !== 'in_hand') {
739
+ return this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.CTRL_POST_BB_NOT_IN_HAND));
740
+ }
741
+ if (_classPrivateFieldGet(_hand, this).stage !== _stage.StageEnum.PRE_FLOP) {
742
+ return this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.CTRL_POST_BB_NOT_PREFLOP));
743
+ }
744
+ if (_classPrivateFieldGet(_hand, this).activePlayer === player) {
745
+ return this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.CTRL_POST_BB_IS_ACTIVE_PLAYER));
746
+ }
747
+ if (player.getStatus() !== 'eligible') {
748
+ return this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.CTRL_POST_BB_PLAYER_INELIGIBLE));
749
+ }
750
+ if (player.currentStageTotalAmount !== 0) {
751
+ return this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.CTRL_POST_BB_ALREADY_CONTRIBUTED));
752
+ }
753
+ var requested = _classPrivateFieldGet(_dealer, this).stakes.bigBlind;
754
+ var balanceBefore = player.balance;
755
+ if (balanceBefore <= 0) {
756
+ return this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.CTRL_POST_BB_NO_CHIPS));
757
+ }
758
+ var posted = _assertClassBrand(_Controller_brand, this, _postBlind).call(this, player, requested);
759
+ player.grantJoiningBlindOption();
760
+ this.recordPotUpdated();
761
+ return {
762
+ userId: player.getUserInfo().id,
763
+ amount: posted,
764
+ requested: requested
765
+ };
766
+ }
767
+ function _emitHandStarted() {
768
+ _classPrivateFieldGet(_handEvents, this).push({
769
+ type: 'HandStarted',
770
+ payload: _assertClassBrand(_Controller_brand, this, _eventMeta).call(this)
771
+ });
772
+ }
773
+ /** 桌 SB/BB、`BlindsPosted`,再 `transferControlTo(BB.getNext())`(翻前首动);不含 `HandStarted`。 */
774
+ function _postTableBlindsAndFirstPreflopActor() {
775
+ var _this4 = this;
776
+ var takeDefaultActionPlayers = _assertClassBrand(_Controller_brand, this, _getSmallBindAndBigBind).call(this);
777
+ var posts = [];
778
+ takeDefaultActionPlayers.forEach(function (player, index) {
779
+ if (player) {
780
+ var requested = index === 0 ? _classPrivateFieldGet(_dealer, _this4).stakes.smallBlind : _classPrivateFieldGet(_dealer, _this4).stakes.bigBlind;
781
+ var kind = index === 0 ? 'sb' : 'bb';
782
+ var posted = _assertClassBrand(_Controller_brand, _this4, _postBlind).call(_this4, player, requested);
783
+ posts.push({
784
+ userId: player.getUserInfo().id,
785
+ amount: posted,
786
+ kind: kind
787
+ });
788
+ _this4.recordPotUpdated();
789
+ }
790
+ });
791
+ _classPrivateFieldGet(_handEvents, this).push({
792
+ type: 'BlindsPosted',
793
+ payload: _objectSpread(_objectSpread({}, _assertClassBrand(_Controller_brand, this, _eventMeta).call(this)), {}, {
794
+ posts: posts
795
+ })
796
+ });
797
+ var _takeDefaultActionPla = _slicedToArray(takeDefaultActionPlayers, 2),
798
+ bigBlind = _takeDefaultActionPla[1];
799
+ var activePlayer = bigBlind === null || bigBlind === void 0 ? void 0 : bigBlind.getNextPlayer();
800
+ if (activePlayer) {
801
+ this.transferControlTo(activePlayer);
802
+ } else {
803
+ return this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.CTRL_START_NO_ACTIVE));
804
+ }
805
+ }
763
806
  function _getSmallBindAndBigBind() {
764
807
  var _classPrivateFieldGet6;
765
808
  var smallBind = _classPrivateFieldGet(_dealer, this).count === 2 ? _classPrivateFieldGet(_dealer, this).button : (_classPrivateFieldGet6 = _classPrivateFieldGet(_dealer, this).button) === null || _classPrivateFieldGet6 === void 0 ? void 0 : _classPrivateFieldGet6.getNextPlayer();
@@ -769,6 +812,21 @@ function _getSmallBindAndBigBind() {
769
812
  })) return this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.CTRL_SB_BB_MISSING));
770
813
  return result;
771
814
  }
815
+ /** 进入翻前会话帧(`in_hand` / `PRE_FLOP`、清队列);须已有 `handId`;**不写**领域事件。 */
816
+ function _enterPreflopHandFrame() {
817
+ if (_classPrivateFieldGet(_activeHandId, this) === null) {
818
+ return this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.CTRL_START_NO_HAND_PREP));
819
+ }
820
+ _classPrivateFieldSet(_pendingTurnEndedReason, this, null);
821
+ _classPrivateFieldSet(_pendingFlowOps, this, []);
822
+ _classPrivateFieldSet(_pausedActiveUserId, this, null);
823
+ _classPrivateFieldSet(_runoutMode, this, false);
824
+ _classPrivateFieldGet(_hand, this).status = 'in_hand';
825
+ _classPrivateFieldGet(_hand, this).stage = _stage.StageEnum.PRE_FLOP;
826
+ if (_TexasEngineContext.TexasEngineContext.simulation().resetDealerBeforeHandStart) {
827
+ _classPrivateFieldGet(_dealer, this).reset();
828
+ }
829
+ }
772
830
  function _settle() {
773
831
  var commonPokes = this.getRevealedPokes();
774
832
  _classPrivateFieldGet(_hand, this).settlement.settleFromCommonBoard(_classPrivateFieldGet(_dealer, this).players, _classPrivateFieldGet(_dealer, this).getPlayersStillInGame(), commonPokes);
@@ -23,6 +23,39 @@ function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("C
23
23
  function _classPrivateFieldGet(s, a) { return s.get(_assertClassBrand(s, a)); }
24
24
  function _classPrivateFieldSet(s, a, r) { return s.set(_assertClassBrand(s, a), r), r; }
25
25
  function _assertClassBrand(e, t, n) { if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n; throw new TypeError("Private element is not present on this object"); }
26
+ var TWO_POW_32 = 0x100000000;
27
+
28
+ /**
29
+ * 均匀整数 `j` 满足 `0 <= j < n`(用于 Fisher–Yates 时 `n = maxInclusive + 1`)。
30
+ * - **生产 / RN**:`crypto.getRandomValues` + 拒绝采样,避免 `uint32 % n` 的 modulo bias;比 `Math.random` 更不可预测。
31
+ * - **Jest**:`NODE_ENV === 'test'` 时只用 `Math.random`,以便 `jest.spyOn(Math, 'random')` 固定发牌序(golden / imperative 对拍)。
32
+ *
33
+ * 更稳妥的架构化做法是构造 `Deck` 时注入 `() => number` 或 `randomInt(max)`;当前以零配置为先。
34
+ */
35
+ function randomIntBelow(n) {
36
+ if (n <= 0) {
37
+ throw new Error("Deck shuffle: invalid range n=".concat(n));
38
+ }
39
+ var inJestTest = typeof process !== 'undefined' && process.env.NODE_ENV === 'test';
40
+ if (!inJestTest) {
41
+ var c = globalThis.crypto;
42
+ if (c !== null && c !== void 0 && c.getRandomValues) {
43
+ var limit = TWO_POW_32 - TWO_POW_32 % n;
44
+ var buf = new Uint32Array(1);
45
+ var v;
46
+ do {
47
+ c.getRandomValues(buf);
48
+ v = buf[0];
49
+ } while (v >= limit);
50
+ return v % n;
51
+ }
52
+ }
53
+ return Math.floor(Math.random() * n);
54
+ }
55
+ function randomIntInclusive(maxInclusive) {
56
+ return randomIntBelow(maxInclusive + 1);
57
+ }
58
+
26
59
  /**
27
60
  * 52 张牌堆:生成、洗牌、按德州规则发手牌与公牌(含烧牌)。
28
61
  * 发牌结果由调用方写入 {@link DealtBoard},本类不缓存手牌/公牌。
@@ -85,9 +118,10 @@ var Deck = /*#__PURE__*/function () {
85
118
  function _createDeck() {
86
119
  _classPrivateFieldSet(_deck, this, (0, _standardDeck.createStandardDeckPokes)());
87
120
  }
121
+ /** Fisher–Yates;索引用 `crypto.getRandomValues`(若可用),否则回退 `Math.random`。 */
88
122
  function _shuffle() {
89
123
  for (var i = _classPrivateFieldGet(_deck, this).length - 1; i > 0; i--) {
90
- var j = Math.floor(Math.random() * (i + 1));
124
+ var j = randomIntInclusive(i);
91
125
  var _ref = [_classPrivateFieldGet(_deck, this)[j], _classPrivateFieldGet(_deck, this)[i]];
92
126
  _classPrivateFieldGet(_deck, this)[i] = _ref[0];
93
127
  _classPrivateFieldGet(_deck, this)[j] = _ref[1];
@@ -51,7 +51,7 @@ function resolveAllowedActions(ctx) {
51
51
  };
52
52
  var last = (_ctx$dealerActionHist = ctx.dealerActionHistory[ctx.dealerActionHistory.length - 1]) !== null && _ctx$dealerActionHist !== void 0 ? _ctx$dealerActionHist : null;
53
53
  var result = _helper(last);
54
- if (ctx.isBigBlindPreFlopOption && result.includes(_constant.ActionTypeEnum.CALL)) {
54
+ if ((ctx.isBigBlindPreFlopOption || ctx.isJoiningBlindPreFlopOption) && result.includes(_constant.ActionTypeEnum.CALL) && ctx.selfCurrentStageTotal >= ctx.maxOthersStageBet) {
55
55
  return [_constant.ActionTypeEnum.CHECK, _constant.ActionTypeEnum.RAISE, _constant.ActionTypeEnum.FOLD, _constant.ActionTypeEnum.ALL_IN];
56
56
  }
57
57
  return result;
@@ -63,6 +63,7 @@ var _handSession = /*#__PURE__*/new WeakMap();
63
63
  var _balance = /*#__PURE__*/new WeakMap();
64
64
  var _status = /*#__PURE__*/new WeakMap();
65
65
  var _action = /*#__PURE__*/new WeakMap();
66
+ var _hasJoiningBlindOption = /*#__PURE__*/new WeakMap();
66
67
  var _stakes = /*#__PURE__*/new WeakMap();
67
68
  var _currentStageTotalAmount = /*#__PURE__*/new WeakMap();
68
69
  var _totalBetAmount = /*#__PURE__*/new WeakMap();
@@ -100,6 +101,7 @@ var Player = exports.Player = /*#__PURE__*/function () {
100
101
  * 业务「多调一次 flush」通常队头已非 `turn_handoff`,靠队列即可挡,不依赖本标记。
101
102
  */
102
103
  _classPrivateFieldInitSpec(this, _action, void 0);
104
+ _classPrivateFieldInitSpec(this, _hasJoiningBlindOption, false);
103
105
  _classPrivateFieldInitSpec(this, _stakes, void 0);
104
106
  /**
105
107
  * 当前阶段的下注总额, 全押筹码不够时可以小于此金额
@@ -199,7 +201,8 @@ var Player = exports.Player = /*#__PURE__*/function () {
199
201
  selfCurrentStageTotal: _classPrivateFieldGet(_currentStageTotalAmount, this),
200
202
  dealerActionHistory: _classPrivateFieldGet(_dealerRing, this).actionHistory,
201
203
  maxOthersStageBet: this.getMaxOthersStageBet(),
202
- isBigBlindPreFlopOption: _assertClassBrand(_Player_brand, this, _isBigBlindOptionInPreFlop).call(this)
204
+ isBigBlindPreFlopOption: _assertClassBrand(_Player_brand, this, _isBigBlindOptionInPreFlop).call(this),
205
+ isJoiningBlindPreFlopOption: _assertClassBrand(_Player_brand, this, _isJoiningBlindOptionInPreFlop).call(this)
203
206
  };
204
207
  }
205
208
 
@@ -320,6 +323,7 @@ var Player = exports.Player = /*#__PURE__*/function () {
320
323
  }, {
321
324
  key: "notifyActionCommitted",
322
325
  value: function notifyActionCommitted(options) {
326
+ _classPrivateFieldSet(_hasJoiningBlindOption, this, false);
323
327
  _classPrivateFieldGet(_handSession, this).recordPlayerAction(this, {
324
328
  emitPot: options.emitPot
325
329
  });
@@ -403,8 +407,8 @@ var Player = exports.Player = /*#__PURE__*/function () {
403
407
  if (_classPrivateFieldGet(_status, this) === 'allIn' || _classPrivateFieldGet(_status, this) === 'out') return false;
404
408
  if (!_classPrivateFieldGet(_action, this)) return true;
405
409
 
406
- // 翻牌前大盲具有最后行动权: 仅下过盲注视为未行动, 须给一次选择机会
407
- if (_assertClassBrand(_Player_brand, this, _isBigBlindOptionInPreFlop).call(this) && _classPrivateFieldGet(_action, this).type === _constant.ActionTypeEnum.BET) return true;
410
+ // 翻牌前盲注选项:大盲或入座贴盲玩家仅下过盲注视为未行动,须给一次选择机会
411
+ if ((_assertClassBrand(_Player_brand, this, _isBigBlindOptionInPreFlop).call(this) || _assertClassBrand(_Player_brand, this, _isJoiningBlindOptionInPreFlop).call(this)) && _classPrivateFieldGet(_action, this).type === _constant.ActionTypeEnum.BET) return true;
408
412
 
409
413
  // 当前的下注金额已经等于最大下注额
410
414
  return _classPrivateFieldGet(_currentStageTotalAmount, this) !== this.getMaxBetAmountAtCurrentStage();
@@ -418,6 +422,7 @@ var Player = exports.Player = /*#__PURE__*/function () {
418
422
  key: "resetAction",
419
423
  value: function resetAction() {
420
424
  _classPrivateFieldSet(_action, this, undefined);
425
+ _classPrivateFieldSet(_hasJoiningBlindOption, this, false);
421
426
  }
422
427
  }, {
423
428
  key: "getAllowedActions",
@@ -640,6 +645,13 @@ var Player = exports.Player = /*#__PURE__*/function () {
640
645
  _classPrivateFieldGet(_handSession, this).recordTurnOffered(this);
641
646
  this.continue();
642
647
  }
648
+
649
+ /** 贴入座大盲后保留一次翻前主动选择权(直到该玩家提交一次自愿行动)。 */
650
+ }, {
651
+ key: "grantJoiningBlindOption",
652
+ value: function grantJoiningBlindOption() {
653
+ _classPrivateFieldSet(_hasJoiningBlindOption, this, true);
654
+ }
643
655
  }]);
644
656
  }();
645
657
  /**
@@ -649,6 +661,12 @@ var Player = exports.Player = /*#__PURE__*/function () {
649
661
  function _isBigBlindOptionInPreFlop() {
650
662
  return _classPrivateFieldGet(_handSession, this).stage === _stage.StageEnum.PRE_FLOP && _classPrivateFieldGet(_role, this) === _constant.RoleEnum.BB && _classPrivateFieldGet(_currentStageTotalAmount, this) >= this.getMaxBetAmountAtCurrentStage();
651
663
  }
664
+ /**
665
+ * 翻牌前入座贴盲选项:已贴到当前最高注且尚未消耗该次行动机会时,可选择过牌/加注/弃牌/全下。
666
+ */
667
+ function _isJoiningBlindOptionInPreFlop() {
668
+ return _classPrivateFieldGet(_handSession, this).stage === _stage.StageEnum.PRE_FLOP && _classPrivateFieldGet(_hasJoiningBlindOption, this) && _classPrivateFieldGet(_currentStageTotalAmount, this) >= this.getMaxBetAmountAtCurrentStage();
669
+ }
652
670
  function _getAllowedActions() {
653
671
  return (0, _allowedActions.resolveAllowedActions)(this.getAllowedActionsContext());
654
672
  }
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
 
3
+ function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
3
4
  Object.defineProperty(exports, "__esModule", {
4
5
  value: true
5
6
  });
@@ -28,7 +29,6 @@ var _tableCommandParse = require("../domain/tableCommandParse");
28
29
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); }
29
30
  function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != _typeof(e) && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
30
31
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
31
- function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
32
32
  function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
33
33
  function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
34
34
  function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
@@ -39,9 +39,12 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
39
39
  function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); }
40
40
  function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } }
41
41
  function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; }
42
+ function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); }
43
+ function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); }
42
44
  function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
43
45
  function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
44
46
  function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
47
+ function _assertClassBrand(e, t, n) { if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n; throw new TypeError("Private element is not present on this object"); }
45
48
  /** 当前引擎角色表最多支持 10 人桌;更大人数需扩展 playerRoleSetMap */
46
49
  var SUPPORTED_MAX_TABLE_PLAYERS = 10;
47
50
  /**
@@ -50,6 +53,7 @@ var SUPPORTED_MAX_TABLE_PLAYERS = 10;
50
53
  * {@link TexasDomainEvent},与文档中 `apply → events[]` 形态对齐;仍可用 {@link drainDomainEvents} 取出
51
54
  * 未经上述 API 消费的缓冲(少见)。进街/交权节奏由 {@link getPendingFlowOps} + `flush*` / `apply*` 驱动。
52
55
  */
56
+ var _Texas_brand = /*#__PURE__*/new WeakSet();
53
57
  var Texas = /*#__PURE__*/function () {
54
58
  function Texas(_ref) {
55
59
  var user = _ref.user,
@@ -57,6 +61,8 @@ var Texas = /*#__PURE__*/function () {
57
61
  maximumCountOfPlayers = _ref.maximumCountOfPlayers,
58
62
  initialChips = _ref.initialChips;
59
63
  _classCallCheck(this, Texas);
64
+ /** `start` / `startPreflopWithJoiningBigBlinds` 共用的开局前置校验。 */
65
+ _classPrivateMethodInitSpec(this, _Texas_brand);
60
66
  _defineProperty(this, "pool", void 0);
61
67
  _defineProperty(this, "room", void 0);
62
68
  _defineProperty(this, "dealer", void 0);
@@ -232,23 +238,32 @@ var Texas = /*#__PURE__*/function () {
232
238
  value: function removePlayerByIdAsSystem(userId) {
233
239
  this.room.removeByIdForce(userId);
234
240
  }
235
-
241
+ }, {
242
+ key: "start",
243
+ value:
236
244
  /**
237
245
  * 开始本手:`HandStarted` / 盲注等事件进入缓冲,且队列入队首人 `turn_handoff`。
238
246
  * 须随后 drain 并消费队列,首条 `TurnOffered` 才会出现。
239
247
  */
240
- }, {
241
- key: "start",
242
- value: function start() {
243
- if (this.room.getPlayersBySeatStatus('on-set').length < 2) this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.SESSION_START_MIN_SEATED, {
244
- min: 2
245
- }));
246
- if (this.room.status === 'seats_open') this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.SESSION_START_SEATS_OPEN));
247
- if (this.controller.status !== 'idle') this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.SESSION_START_NOT_IDLE));
248
+ function start() {
249
+ _assertClassBrand(_Texas_brand, this, _assertCanStartHand).call(this);
248
250
  this.controller.start();
249
251
  return this.drainDomainEvents();
250
252
  }
251
253
 
254
+ /**
255
+ * 开始本手(含「入座大盲」):与 {@link start} 相同前置条件;由 Core 原子完成
256
+ * `HandStarted` → 对 `joiningBigBlindUserIds` 贴入座大盲(跳过 SB/BB、未知 id 忽略)→ **恒** 一条 `PostedJoiningBigBlinds`(`posts` 汇总,无人须贴时为空数组)→ 桌盲 → `transferControlTo(BB.getNext())`。
257
+ * 业务应消费本批领域事件并驱动 `pendingFlowOps`;勿再于 `start()` 后自行 `dispatchCommand(PostBigBlind)`。
258
+ */
259
+ }, {
260
+ key: "startPreflopWithJoiningBigBlinds",
261
+ value: function startPreflopWithJoiningBigBlinds(joiningBigBlindUserIds) {
262
+ _assertClassBrand(_Texas_brand, this, _assertCanStartHand).call(this);
263
+ this.controller.startPreflopWithJoiningBigBlinds(joiningBigBlindUserIds);
264
+ return this.drainDomainEvents();
265
+ }
266
+
252
267
  /** 强制结束本手到 `between_hands`;状态校验由 {@link Controller.end} 负责(非 `in_hand` 时 `CTRL_END_NOT_IN_HAND`)。 */
253
268
  }, {
254
269
  key: "end",
@@ -450,4 +465,11 @@ var Texas = /*#__PURE__*/function () {
450
465
  }
451
466
  }]);
452
467
  }();
468
+ function _assertCanStartHand() {
469
+ if (this.room.getPlayersBySeatStatus('on-set').length < 2) this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.SESSION_START_MIN_SEATED, {
470
+ min: 2
471
+ }));
472
+ if (this.room.status === 'seats_open') this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.SESSION_START_SEATS_OPEN));
473
+ if (this.controller.status !== 'idle') this.fail(new _TexasError.default(_TexasError.TexasCoreErrorCode.SESSION_START_NOT_IDLE));
474
+ }
453
475
  var _default = exports.default = Texas;
@@ -18,7 +18,7 @@ function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e
18
18
  * 应用启动时 `configure`,单元测试可 `reset` 或按需 `configure`。
19
19
  */
20
20
 
21
- /** 与旧 PROJECT_ENV=dev 等行为对齐的可选仿真开关 */
21
+ /** 进程级可选仿真开关(由 `Texas.configureEngine` / `TexasEngineContext.configure` 注入) */
22
22
 
23
23
  var DEFAULT_GLOBAL = {};
24
24
  var TexasEngineContext = exports.TexasEngineContext = /*#__PURE__*/function () {
@@ -384,7 +384,7 @@ function reduceTurnEndedTrailFromDomainEvents(events) {
384
384
  return rows;
385
385
  }
386
386
 
387
- /** 本批中**最后一条** `PostedBigBlind`(中途入座大盲)。 */
387
+ /** 本批中**最后一条** `PostedJoiningBigBlinds` 且 `posts` 非空时,取其 `posts` 末项(`PostBigBlind` 为单元素;批量开局为多行汇总)。`posts` 为空的事件跳过。 */
388
388
 
389
389
  function reduceLastPostedBigBlindFromDomainEvents(events) {
390
390
  var last = null;
@@ -393,11 +393,14 @@ function reduceLastPostedBigBlindFromDomainEvents(events) {
393
393
  try {
394
394
  for (_iterator13.s(); !(_step13 = _iterator13.n()).done;) {
395
395
  var e = _step13.value;
396
- if (e.type === 'PostedBigBlind') {
397
- var p = e.payload;
396
+ if (e.type === 'PostedJoiningBigBlinds') {
397
+ var posts = e.payload.posts;
398
+ if (posts.length === 0) continue;
399
+ var p = posts[posts.length - 1];
400
+ var meta = e.payload;
398
401
  last = {
399
- handId: p.handId,
400
- seq: p.seq,
402
+ handId: meta.handId,
403
+ seq: meta.seq,
401
404
  userId: p.userId,
402
405
  amount: p.amount,
403
406
  requested: p.requested
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "texas-poker-core",
3
- "version": "1.4.26",
3
+ "version": "1.4.28",
4
4
  "description": "德州扑克核心功能",
5
5
  "main": "dist/index.js",
6
6
  "types": "types/index.d.ts",
7
7
  "scripts": {
8
- "test": "cross-env PROJECT_ENV=prd jest",
8
+ "test": "jest",
9
9
  "prettier": "prettier --write \"**/*.{js,ts}\"",
10
10
  "prepare": "husky",
11
11
  "commitlint": "commitlint --edit",
@@ -14,15 +14,15 @@
14
14
  "prepublishOnly": "tsc --noEmit && pnpm run test",
15
15
  "build:types": "node -e \"require('fs').rmSync('types',{recursive:true,force:true})\" && npx tsc && npx tsc-alias",
16
16
  "build:core": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && babel src --out-dir dist --extensions \".ts\"",
17
- "build": "cross-env PROJECT_ENV=prd pnpm run build:types && pnpm run build:core",
17
+ "build": "pnpm run build:types && pnpm run build:core",
18
18
  "push-origin": "ts-node ./publish",
19
19
  "push-local": "bash scripts/push-local.sh",
20
- "integration-test": "cross-env PROJECT_ENV=dev ts-node -r tsconfig-paths/register ./src/integration_test.ts",
21
- "simulatore": "cross-env PROJECT_ENV=prd ts-node -r tsconfig-paths/register ./src/simulator_game_run.ts",
22
- "replay:composite": "cross-env PROJECT_ENV=prd ts-node -r tsconfig-paths/register ./src/replay/replayCompositeCli.ts",
23
- "replay:validate": "cross-env PROJECT_ENV=prd ts-node -r tsconfig-paths/register ./src/replay/replayValidateCli.ts",
24
- "replay:verify": "cross-env PROJECT_ENV=prd ts-node -r tsconfig-paths/register ./src/replay/replayVerifyCli.ts",
25
- "replay:canonical": "cross-env PROJECT_ENV=prd ts-node -r tsconfig-paths/register ./src/replay/replayCanonicalCli.ts"
20
+ "integration-test": "ts-node -r tsconfig-paths/register ./src/integration_test.ts",
21
+ "simulatore": "ts-node -r tsconfig-paths/register ./src/simulator_game_run.ts",
22
+ "replay:composite": "ts-node -r tsconfig-paths/register ./src/replay/replayCompositeCli.ts",
23
+ "replay:validate": "ts-node -r tsconfig-paths/register ./src/replay/replayValidateCli.ts",
24
+ "replay:verify": "ts-node -r tsconfig-paths/register ./src/replay/replayVerifyCli.ts",
25
+ "replay:canonical": "ts-node -r tsconfig-paths/register ./src/replay/replayCanonicalCli.ts"
26
26
  },
27
27
  "files": [
28
28
  "dist/**/*",
@@ -50,7 +50,6 @@
50
50
  "@types/ramda": "^0.30.2",
51
51
  "babel-plugin-module-resolver": "^5.0.2",
52
52
  "commitlint": "^19.7.1",
53
- "cross-env": "^7.0.3",
54
53
  "eslint": "^9.21.0",
55
54
  "globals": "^16.0.0",
56
55
  "husky": "^9.1.7",
@@ -10,8 +10,8 @@ import type { HandDomainEvent, TurnEndedReason } from '../domain/handDomainEvent
10
10
  import type { GameComponent, HandLifecycle, TexasErrorCallback } from '../gameContracts';
11
11
  import Pool from '../Pool';
12
12
  import Dealer from '../Dealer';
13
- import { Player } from '../Player';
14
13
  import { Poke } from '../Deck/constant';
14
+ import { Player } from '../Player';
15
15
  import { StageEnum, type Stage } from './stage';
16
16
  export { StageEnum, type Stage } from './stage';
17
17
  export type { HandLifecycle };
@@ -121,12 +121,20 @@ declare class Controller implements GameComponent, PlayerHandSession<Player> {
121
121
  * 入池额 `min(桌大盲, 余额)`,与开局盲注路径一致;**不**调用 `completeBettingTurn`、不改变当前思考权。
122
122
  */
123
123
  postBigBlindForJoiningPlayer(player: Player): void;
124
- takeActionInPreFlop(): undefined;
124
+ takeActionInPreFlop(): void;
125
125
  /**
126
126
  * 进入本手街道阶段:须在 {@link prepareHandTape} 之后调用;不清空已缓冲的 `RolesAssigned` / `HoleCardsDealt`。
127
127
  * 写入 `HandStarted`、贴盲与 **`pendingFlowOps` / 跑马路状态**,再 `transferControlTo`(首人思考权先入队,待业务 flush)。
128
128
  */
129
- start(): undefined;
129
+ start(): void;
130
+ /**
131
+ * 与 {@link start} 同属一手开局原子路径:先进入翻前帧并 `HandStarted`,再对 `joiningBigBlindUserIds`
132
+ * 依次入账(跳过 SB/BB、环上不存在 id 忽略),其间每条入池后各一条 `PotUpdated`;
133
+ * 最后 **恒** 缓冲一条 {@link PostedJoiningBigBlinds},`posts` 为本次所有入座大盲(无人须贴时为空数组);
134
+ * 再贴桌盲并 `transferControlTo(BB.getNext())`。
135
+ * 业务应仅此入口处理「待贴入座大盲」,勿在 `start()` 后再穿插 `PostBigBlind`。
136
+ */
137
+ startPreflopWithJoiningBigBlinds(joiningBigBlindUserIds: ReadonlyArray<number>): void;
130
138
  continue(): undefined;
131
139
  settleRankingsThroughStage(throughStage: Stage): void;
132
140
  end(): undefined;
@@ -17,6 +17,7 @@ export type AllowedActionsContext = {
17
17
  }[];
18
18
  maxOthersStageBet: number;
19
19
  isBigBlindPreFlopOption: boolean;
20
+ isJoiningBlindPreFlopOption: boolean;
20
21
  };
21
22
  export declare function resolveAllowedActions(ctx: AllowedActionsContext): ActionTypeEnum[];
22
23
  /**
@@ -165,6 +165,8 @@ export declare class Player implements GameComponent {
165
165
  max: number;
166
166
  };
167
167
  getControl(): void;
168
+ /** 贴入座大盲后保留一次翻前主动选择权(直到该玩家提交一次自愿行动)。 */
169
+ grantJoiningBlindOption(): void;
168
170
  }
169
171
  /**
170
172
  * 仍可参与本街思考权轮转(未弃牌、未进入全下终态)。
@@ -24,6 +24,7 @@ export { parseTableCommandFromJson, parseTableCommandFromUnknown } from '../doma
24
24
  * 未经上述 API 消费的缓冲(少见)。进街/交权节奏由 {@link getPendingFlowOps} + `flush*` / `apply*` 驱动。
25
25
  */
26
26
  declare class Texas {
27
+ #private;
27
28
  pool: Pool;
28
29
  room: Room;
29
30
  dealer: Dealer;
@@ -73,6 +74,12 @@ declare class Texas {
73
74
  * 须随后 drain 并消费队列,首条 `TurnOffered` 才会出现。
74
75
  */
75
76
  start(): TexasDomainEvent[];
77
+ /**
78
+ * 开始本手(含「入座大盲」):与 {@link start} 相同前置条件;由 Core 原子完成
79
+ * `HandStarted` → 对 `joiningBigBlindUserIds` 贴入座大盲(跳过 SB/BB、未知 id 忽略)→ **恒** 一条 `PostedJoiningBigBlinds`(`posts` 汇总,无人须贴时为空数组)→ 桌盲 → `transferControlTo(BB.getNext())`。
80
+ * 业务应消费本批领域事件并驱动 `pendingFlowOps`;勿再于 `start()` 后自行 `dispatchCommand(PostBigBlind)`。
81
+ */
82
+ startPreflopWithJoiningBigBlinds(joiningBigBlindUserIds: readonly number[]): TexasDomainEvent[];
76
83
  /** 强制结束本手到 `between_hands`;状态校验由 {@link Controller.end} 负责(非 `in_hand` 时 `CTRL_END_NOT_IN_HAND`)。 */
77
84
  end(): TexasDomainEvent[];
78
85
  /** 中央池分配给赢家并缓冲 `PotAwarded`(通常在解释 `HandEnded` 时调用)。 */
@@ -2,7 +2,7 @@
2
2
  * 进程级引擎配置:仿真开关等(**不含**库内 trace;观测由业务对领域事件 / 指令自行记录)。
3
3
  * 应用启动时 `configure`,单元测试可 `reset` 或按需 `configure`。
4
4
  */
5
- /** 与旧 PROJECT_ENV=dev 等行为对齐的可选仿真开关 */
5
+ /** 进程级可选仿真开关(由 `Texas.configureEngine` / `TexasEngineContext.configure` 注入) */
6
6
  export type TexasSimulationFlags = {
7
7
  /** balance setter 不生效 */
8
8
  ignoreBalanceSetter?: boolean;
@@ -22,7 +22,7 @@ export type HandEventMeta = {
22
22
  *
23
23
  * **顺序约定(终极目标 / 回放友好)**
24
24
  * - 自愿下注:`PlayerActed` 与同一次入池后的 **`PotUpdated` 紧邻**,且 **`PlayerActed` 在前**(见 `Controller.recordPlayerAction`)。
25
- * - 盲注:每次 `#postBlind` 入池后各一条 **`PotUpdated`**(细粒度),再以 **`BlindsPosted`** 汇总;不发 `PlayerActed`。
25
+ * - 盲注:每次 `#postBlind` 入池后各一条 **`PotUpdated`**(细粒度)。**入座大盲**:该批每位玩家的 `PotUpdated` **之后** 紧跟 **`PostedJoiningBigBlinds`**(`Texas#startPreflopWithJoiningBigBlinds` **恒** 一条、`posts` 汇总当次入账,无人须贴时 `posts` 为 `[]`;`PostBigBlind` 指令则每次一条且 `posts.length === 1`)。**桌 SB/BB**:各自 `PotUpdated` 之后 **`BlindsPosted`** 汇总;不发 `PlayerActed`。
26
26
  * - `seq` 在 {@link HandEventMeta} 中本手单调递增,与 `handId` 联用做幂等与重放键。
27
27
  */
28
28
  export type HandDomainEvent = {
@@ -53,14 +53,21 @@ export type HandDomainEvent = {
53
53
  kind: 'sb' | 'bb';
54
54
  }>;
55
55
  };
56
- } | {
57
- type: 'PostedBigBlind';
56
+ }
57
+ /**
58
+ * 中途入座大盲汇总(与桌盲 {@link BlindsPosted} 区分:`requested` 为桌级大盲规定额,`amount` 为实际入池)。
59
+ *
60
+ * - {@link Texas#startPreflopWithJoiningBigBlinds}:每名待贴玩家先入账并各发 **`PotUpdated`**,再 **恒** 本条,`posts` 为当次全部行(经 SB/BB/未知 id 跳过后无人须贴则为 `[]`)。
61
+ * - `dispatchCommand({ type: 'PostBigBlind' })`:先 **`PotUpdated`**,再本条,`posts` 恒为单元素。
62
+ */
63
+ | {
64
+ type: 'PostedJoiningBigBlinds';
58
65
  payload: HandEventMeta & {
59
- userId: number;
60
- /** 实际入池(短码时为 `min(requested, balance)`) */
61
- amount: number;
62
- /** 桌级大盲规定额 */
63
- requested: number;
66
+ posts: Array<{
67
+ userId: number;
68
+ amount: number;
69
+ requested: number;
70
+ }>;
64
71
  };
65
72
  } | {
66
73
  type: 'PlayerActed';
@@ -46,10 +46,15 @@ export type TableCommand = {
46
46
  playerId: number;
47
47
  }
48
48
  /**
49
- * 中途入座:在翻牌前、非当前思考方、且本街尚未入池时,按桌级大盲强制贴盲(`min(BB, 余额)`),
49
+ * 中途入座:在翻牌前、且本街尚未入池时,按桌级大盲强制贴盲(`min(BB, 余额)`),
50
50
  * 不交权、不触发自愿行动校验;金额由 core 从 `Dealer.stakes.bigBlind` 推导,不由业务传参。
51
+ *
52
+ * 默认仍拒绝 **当前 `activePlayer`**(防滥用自愿行动绕过)。业务在「一手开局后批量贴入座大盲」时,
53
+ * 若某待贴玩家恰为翻前首动位(`BB.getNext()`),须传 **`allowWhenCurrentActor: true`**(仅服务端对可信入座队列使用)。
51
54
  */
52
55
  | {
53
56
  type: 'PostBigBlind';
54
57
  playerId: number;
58
+ /** 为 true 时允许当前思考方贴入座大盲(须仍满足 `currentStageTotalAmount === 0` 等其余校验) */
59
+ allowWhenCurrentActor?: boolean;
55
60
  };
@@ -102,7 +102,7 @@ export type TurnEndedEntry = Readonly<{
102
102
  reason: TurnEndedReason;
103
103
  }>;
104
104
  export declare function reduceTurnEndedTrailFromDomainEvents(events: readonly TexasDomainEvent[]): readonly TurnEndedEntry[];
105
- /** 本批中**最后一条** `PostedBigBlind`(中途入座大盲)。 */
105
+ /** 本批中**最后一条** `PostedJoiningBigBlinds` 且 `posts` 非空时,取其 `posts` 末项(`PostBigBlind` 为单元素;批量开局为多行汇总)。`posts` 为空的事件跳过。 */
106
106
  export type PostedBigBlindReadModel = Readonly<{
107
107
  handId: string;
108
108
  seq: number;