texas-poker-core 1.4.29 → 1.4.31

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
@@ -1,11 +1,14 @@
1
1
  # Texas-Poker-Core
2
2
 
3
3
  无服务、无持久化的 **德州扑克(Texas Hold’em)对局引擎**:房间与座位、盲注与角色、发牌、行动轮次、阶段推进、摊牌比牌、奖池与边池分配等规则均在库内完成。
4
- **不包含**:账号系统、WebSocket/HTTP、数据库、匹配、UI;这些由业务层接入 `Texas` / `Room` / `Player` 的 API 与回调实现。
4
+ **不包含**:账号系统、WebSocket/HTTP、数据库、匹配、UI、节拍与 `sleep`;这些由业务层解释 **领域事件** 并驱动 **`pendingFlowOps`** 队列实现。
5
5
 
6
- - **入口类**:`Texas` — 组装 `Pool`、`Dealer`、`Controller`、`Room`,并提供会话级方法。
7
- - **错误模型**:规则或状态不满足时通过 `TexasError`(`TexasCoreErrorCode`)**fail-fast**;可用 `onError` 先记录再抛出。
8
- - **可选节奏钩子**:构造 `Texas` 时传入 `beforeStageAdvance` / `beforeNextPlayerTurn`,在阶段切换、轮到下家前 `await`(例如发 WS、动画、`sleep`)。
6
+ - **入口类**:`Texas` — 组装 `Pool`、`Dealer`、`Controller`、`Room`;状态变更写入事件缓冲,由业务 **`drainDomainEvents()`** 取出后落库 / 推送。
7
+ - **指令入口**:玩家行动统一走 **`dispatchCommand(TableCommand)`**(自愿下注、超时、离场弃牌、入座大盲等)。
8
+ - **节奏解耦**:进街与交权进入 **`pendingFlowOps`**(`stage_advance` | `turn_handoff`),业务按产品节拍调用 **`applyPendingStageAdvance`** / **`flushPendingTurnHandoff`**(单测可 **`flushAllPendingFlowOps`** 一次排空)。
9
+ - **错误模型**:规则或状态不满足时 **`TexasError`(`TexasCoreErrorCode`)** fail-fast;`message` 为默认中文,业务可用 `code` + `payload` 做 i18n。
10
+
11
+ **详细 API 清单**:[CORE_API.md](./CORE_API.md)。**与参考服务端的一整局事件流**:[docs/integration-core-wish-event-flow.md](./docs/integration-core-wish-event-flow.md)。
9
12
 
10
13
  ---
11
14
 
@@ -24,12 +27,12 @@ npm install texas-poker-core
24
27
 
25
28
  | 对象 | 职责 |
26
29
  | ------------ | --------------------------------------------------------------------------------------------------------------------------------- |
27
- | `Texas` | 创建一桌、注册监听、`setPlayerRoles` / `dealCards` / `start` / `settle` / `reset` 等会话流程 |
30
+ | `Texas` | 会话门面:`setPlayerRoles` / `dealCards` / `start` / `dispatchCommand` / `drainDomainEvents` / 消费 `pendingFlowOps` / `reset` |
28
31
  | `Room` | 成员加入/观战/入座/离座、`initialRoles`(末 `lockSeats`)/ `rotateRoles`(不锁座)、`RoomStatus`(`seats_open` / `seats_locked`) |
29
- | `Dealer` | 盲注、庄家、角色顺序、发牌、行动历史(通常不直接给业务大量调用,多经 `Texas` / `Room`) |
30
- | `Controller` | 一手牌生命周期 `HandLifecycle`、当前街 `stage`、活跃玩家 `activePlayer`、阶段推进与终局 |
31
- | `Pool` | 奖池与支付(`texas.settle()` 时 `pool.pay()`) |
32
- | `Player` | 单个座位的筹码、手牌、行动 `check` / `bet` / `call` / `raise` / `fold` / `allIn` 与 `getControl` |
32
+ | `Dealer` | 盲注、庄家、角色顺序、发牌(通常经 `Texas` / `Room` 访问) |
33
+ | `Controller` | 一手牌 `HandLifecycle`、当前街 `stage`、`activePlayer`、事件缓冲与 `pendingFlowOps` 队列 |
34
+ | `Pool` | 奖池与支付(`texas.settle()` 时 `pool.pay()`,并缓冲 `PotAwarded`) |
35
+ | `Player` | 单座筹码、手牌、允许行动集合;**勿**直接调 `check`/`bet`/…,统一经 `Texas#dispatchCommand` |
33
36
 
34
37
  ---
35
38
 
@@ -52,30 +55,22 @@ npm install texas-poker-core
52
55
 
53
56
  ## 典型对局流程(使用手册)
54
57
 
55
- 以下为常见顺序;具体校验与错误码以运行时 `TexasError` 为准。
58
+ 以下为常见顺序;具体校验与错误码以运行时 `TexasError` 为准。
59
+ **约定**:下列 `setPlayerRoles` / `dealCards` / `start` / `dispatchCommand` 等会 **同步返回** 本步 `TexasDomainEvent[]`(等价于随即 `drainDomainEvents()`);若你自行多次调用 Core 再统一 drain,也可用 `texas.drainDomainEvents()` 取出缓冲。
56
60
 
57
61
  ### 1. 创建牌桌
58
62
 
59
63
  ```ts
60
- import { Texas } from 'texas-poker-core'
64
+ import { Texas, type TableCommand } from 'texas-poker-core'
61
65
 
62
66
  const texas = new Texas({
63
67
  user: { id: 1, name: '房主' },
64
68
  lowestBetAmount: 20,
65
69
  maximumCountOfPlayers: 9,
66
- initialChips: 2000,
67
- // 可选:与 WS/动画对齐
68
- beforeNextPlayerTurn: async () => {
69
- /* await sleep(...) */
70
- },
71
- beforeStageAdvance: async () => {
72
- /* ... */
73
- }
70
+ initialChips: 2000
74
71
  })
75
72
 
76
- texas.onError((err) => {
77
- // 日志、监控;随后仍会 throw
78
- })
73
+ // 业务侧:try/catch TexasError,写日志 / 监控 / 映射 HTTP 状态码
79
74
  ```
80
75
 
81
76
  ### 2. 注册用户与入座
@@ -84,62 +79,155 @@ texas.onError((err) => {
84
79
  const p2 = texas.createPlayer({ id: 2, name: '玩家2' })
85
80
  texas.room.join(p2)
86
81
  texas.room.seat(p2)
87
- // join = 进房(默认观战席);seat = 上桌,且要求房间 seats_open(一手收尾 reset 后)
82
+ // join = 进房(默认观战);seat = 上桌,须 seats_open(一手收尾 reset 后)
88
83
  ```
89
84
 
90
85
  ### 3. 锁座、分配角色、发牌
91
86
 
92
87
  ```ts
93
- texas.setPlayerRoles('initial') // 首局:Room.initialRoles(定庄+setOthers+锁座);或 'rearrange'(仅 reArrangeRoles,须已有庄)
94
- texas.dealCards()
95
- // 上述会触发 onRolesAssigned / onDealCards(若已注册)
96
- // 批量 seat/remove 后:入座/离环时 Dealer 已各调过 reArrangeRoles;若仍希望「最后一次再推角色」,可再调 texas.reArrangeRoles()(不写入 RolesAssigned 缓冲,需自行读 dealer 上各席 role)
88
+ const events1 = texas.setPlayerRoles('initial')
89
+ // 首局:Room.initialRoles(定庄 + setOthers + lockSeats)
90
+ // 'rearrange'(仅 reArrangeRoles,须已有庄);可选 { buttonUserId }
91
+ await interpret(events1) // 业务:落库 / WS;下同
92
+
93
+ const events2 = texas.dealCards()
94
+ // → HoleCardsDealt(byUserId 含全员手牌;出站前按 viewer 过滤)
95
+
96
+ // 批量 seat/remove 后:Dealer 已各调 reArrangeRoles;可再 texas.reArrangeRoles() 统一推角色
97
+ // (不缓冲 RolesAssigned,需自行读 dealer 各席 role)
97
98
  ```
98
99
 
99
100
  ### 4. 开始本手
100
101
 
101
102
  ```ts
102
- // 要求:至少两人 on-set、房间 seats_locked、controller.idle
103
- await texas.start()
104
- // 内部会下盲注并移交控制权到第一个行动玩家
103
+ // 要求:至少 2 人 on-setseats_locked、controller.idle
104
+ const events3 = texas.start()
105
+ // → HandStarted、BlindsPosted、PotUpdated…;队列入队首人 turn_handoff(尚无 TurnOffered)
106
+
107
+ await drainAndConsumePendingFlow(texas, events3)
108
+ // 生产:先解释 events3,再按节拍 applyPendingStageAdvance / flushPendingTurnHandoff 并 drain 新事件
109
+ // 单测:texas.flushAllPendingFlowOps() 一次排空
110
+
111
+ // 有待贴「入座大盲」队列时,用原子 API 替代 start + 循环 PostBigBlind:
112
+ // texas.startPreflopWithJoiningBigBlinds([userId, ...])
105
113
  ```
106
114
 
107
- ### 5. 轮到谁行动
115
+ ### 5. 玩家行动
108
116
 
109
- 当前行动玩家:`texas.controller.activePlayer`。
110
- 该玩家可调用(均为 `async`,内部会校验合法行动并可能推进阶段/终局):
117
+ 当前行动方:`texas.controller.activePlayer`(须已消费队头 `turn_handoff`,否则 `status` 非 `active`,`dispatchCommand` 会被拒)。
111
118
 
112
- - `check()` / `bet(amount)` / `call()` / `raise(amount)` / `fold()` / `allIn()`
119
+ ```ts
120
+ const cmd: TableCommand = { type: 'Call', playerId: 2 }
121
+ const events = texas.dispatchCommand(cmd)
122
+ // 自愿:Fold | Check | Call | Bet | Raise | AllIn
123
+ // 超时:FoldDueToTimeout | CheckDueToTimeout(须当前行动方)
124
+ // 离场:FoldDueToLeave(须当前行动方;TurnEnded.reason === 'leave')
125
+ // 入座大盲:PostBigBlind(翻前、本街尚未入池;金额由 stakes 推导)
126
+
127
+ await drainAndConsumePendingFlow(texas, events)
128
+ ```
113
129
 
114
- 行动前可给每个玩家注册 `onPreAction`,用于推送「允许行动列表、加注区间」等(见下文监听)。
130
+ 轮到谁、允许哪些操作:解释缓冲中的 **`TurnOffered`**(`allowedActions`、`restrict`)。
131
+ 行动结果:**`PlayerActed`** + 紧邻 **`PotUpdated`**;交权结束:**`TurnEnded`**。
115
132
 
116
133
  ### 6. 本手结束与清理
117
134
 
118
- 终局时 `Controller` 会触发 `onGameEnd`(若已注册)。
119
- 之后业务侧通常:
135
+ 终局时缓冲会出现 **`HandEnded`**(`outcome`、`pokesRevealed`、`bestPokes` 等)。业务通常在解释该事件时:
136
+
137
+ ```ts
138
+ const awarded = texas.settle() // pool.pay() → PotAwarded
139
+ await interpret(awarded)
140
+
141
+ texas.reset() // pool + dealer + controller → idle,unlockSeats
142
+ // 下一手:reset → rotateRolesForNewHand → …局间 seat/reArrange… → lockSeats
143
+ // → setPlayerRoles('rearrange')? → dealCards → start() 或 startPreflopWithJoiningBigBlinds
144
+ ```
145
+
146
+ ### 7. 业务侧最小循环(示意)
120
147
 
121
148
  ```ts
122
- texas.settle() // pool.pay(),按引擎规则分配边池
123
- texas.reset() // pool + dealer + controller 清理,controller → idle,并 unlockSeats
124
- // 下一手(示意):reset(unlock) → rotateRolesForNewHand(移庄) → …局间 seat/reArrange… → lockSeats → setPlayerRoles('rearrange')? → dealCards → start()
149
+ import type { Texas, TexasDomainEvent } from 'texas-poker-core'
150
+
151
+ async function drainAndConsumePendingFlow(
152
+ texas: Texas,
153
+ firstBatch: TexasDomainEvent[]
154
+ ) {
155
+ await interpret(firstBatch)
156
+ while (texas.getPendingFlowOps().length > 0) {
157
+ await sleep(actionRequiredMs) // 产品节拍,Core 内无 sleep
158
+ const head = texas.getPendingFlowOps()[0]
159
+ const more =
160
+ head.kind === 'stage_advance'
161
+ ? texas.applyPendingStageAdvance()
162
+ : texas.flushPendingTurnHandoff()
163
+ await interpret(more)
164
+ if (head.kind === 'stage_advance' && texas.getPendingFlowOps().length > 0) {
165
+ await sleep(stageChangedMs)
166
+ }
167
+ }
168
+ }
125
169
  ```
126
170
 
171
+ 单测可省略 `sleep`,在每次 `dispatchCommand` 后调用 **`texas.flushAllPendingFlowOps()`**。
172
+
127
173
  ---
128
174
 
129
- ## 监听与回调(Texas)
175
+ ## 领域事件(`TexasDomainEvent`)
130
176
 
131
- | 方法 | 说明 |
132
- | ----------------- | ------------------------------------------------------------------------ |
133
- | `onError` | 任意 `fail` / `TexasError` 抛出前回调 |
134
- | `onRolesAssigned` | `setPlayerRoles` 成功后,携带 `userId` / `role` / `actionIndex` |
135
- | `onDealCards` | `dealCards` 成功后,各玩家手牌(业务可据此推送私密牌) |
136
- | `onPreAction` | 轮到玩家行动前(注册到所有当前 `Player`) |
137
- | `onAction` | 玩家完成一次合法行动后(写库、广播;默认盲注行为可能不触发,以实现为准) |
138
- | `onGameStart` | 本手 `controller.start()` 内、盲注与首回合开始前 |
139
- | `onGameEnd` | 一手结束,携带公牌、摊牌信息、`pokesRevealed` 等 |
140
- | `onNextStage` | 翻牌 / 转牌 / 河牌等阶段推进,携带本段新亮公牌 |
177
+ 类型定义:`src/domain/handDomainEvents.ts`(包内从 `texas-poker-core` 导出 `TexasDomainEvent` / `HandDomainEvent`)。
178
+ 本手事件均带 **`handId`**(`start` 时分配,如 `h1`)与单调 **`seq`**,供 `(matchId, handId, seq)` 幂等落库与回放。
141
179
 
142
- `Player` 上另有 `onPreAction` / `onAction`,适合按人注册。
180
+ | 事件 | 典型触发时机 |
181
+ | ------------------------ | ---------------------------------------------------------------------------------- |
182
+ | `RolesAssigned` | `setPlayerRoles` |
183
+ | `HoleCardsDealt` | `dealCards` |
184
+ | `HandStarted` | `start` / `startPreflopWithJoiningBigBlinds` |
185
+ | `PostedJoiningBigBlinds` | 入座大盲汇总(`startPreflopWithJoiningBigBlinds` 恒一条;`PostBigBlind` 每次一条) |
186
+ | `BlindsPosted` | 桌 SB/BB 贴盲后 |
187
+ | `PlayerActed` | 自愿行动 / 超时行动(盲注路径不发) |
188
+ | `PotUpdated` | 每次入池后(紧跟对应 `PlayerActed` 或盲注行) |
189
+ | `StageAdvanced` | 消费 `stage_advance`:正常进街或跑马揭示 |
190
+ | `TurnOffered` | 消费 `turn_handoff`:`getControl` 后 |
191
+ | `TurnEnded` | 行动方交权结束(`reason`: `action` \| `timeout` \| `leave` 等) |
192
+ | `PotAwarded` | `settle()` |
193
+ | `HandEnded` | 独赢弃牌或摊牌/跑马结束 |
194
+
195
+ **不再提供** `onGameEnd` / `onAction` / `onPreAction` 等实例回调;节奏由业务解释事件 + 消费 `pendingFlowOps` 完成。
196
+
197
+ 回放与持久化辅助:`toPersistedDomainEventRows`、`projectCompositeReadModel`、`interpret` 等见包导出与 [docs/replay-app-server-core-coordination.md](./docs/replay-app-server-core-coordination.md)。
198
+
199
+ ---
200
+
201
+ ## `TableCommand`(`dispatchCommand`)
202
+
203
+ | `type` | 说明 |
204
+ | ----------------------------------------------------- | -------------------------------------------------------------- |
205
+ | `Fold` / `Check` / `Call` / `Bet` / `Raise` / `AllIn` | 自愿行动;`Bet`/`Raise` 带 `amount` / `additionalAmount` |
206
+ | `FoldDueToTimeout` / `CheckDueToTimeout` | 计时到期;须当前行动方 |
207
+ | `FoldDueToLeave` | 当前行动方离场弃牌;可先 `canFoldDueToLeave(userId)` |
208
+ | `PostBigBlind` | 翻前补入座大盲;可选 `allowWhenCurrentActor`(服务端可信队列) |
209
+
210
+ JSON 解析:`parseTableCommandFromJson` / `parseTableCommandFromUnknown`。
211
+
212
+ ---
213
+
214
+ ## `Texas` 会话 API 速查
215
+
216
+ | 方法 | 说明 |
217
+ | --------------------------------------------------- | ---------------------------------------------------------- |
218
+ | `drainDomainEvents()` | 取出并清空事件缓冲(无副作用) |
219
+ | `dispatchCommand(cmd)` | 玩家行动唯一推荐入口;返回本步事件 |
220
+ | `getPendingFlowOps()` | 流程队列快照(`stage_advance` \| `turn_handoff`) |
221
+ | `applyPendingStageAdvance()` | 消费队头进街/跑马;队头不对抛 `CTRL_FLOW_PENDING_MISMATCH` |
222
+ | `flushPendingTurnHandoff()` | 消费队头交权 → `TurnOffered`;队头不对则静默 return |
223
+ | `flushAllPendingFlowOps()` | 无 sleep 排空队列(单测/批处理) |
224
+ | `setPlayerRoles` / `dealCards` / `start` / `settle` | 返回本步领域事件;`start` 后须消费队列才有 `TurnOffered` |
225
+ | `startPreflopWithJoiningBigBlinds(ids)` | 含入座大盲的原子开局 |
226
+ | `rotateRolesForNewHand()` | `reset` 后局间移庄,不锁座 |
227
+ | `reArrangeRoles()` | 按庄位重算角色,不写 `RolesAssigned` |
228
+ | `lockSeats` / `unlockSeats` | 显式锁座;`reset()` 会 `unlockSeats` |
229
+ | `removePlayerByIdAsSystem` | 系统级踢人(可绕过 `seats_locked`) |
230
+ | `canFoldDueToLeave` / `end` | 离场弃牌预判 / 强制结束到 `between_hands` |
143
231
 
144
232
  ---
145
233
 
@@ -150,8 +238,8 @@ texas.reset() // pool + dealer + controller 清理,controller → idle,并 u
150
238
  - `watch` / `watchById`:回观战(须 `seats_open`)
151
239
  - `remove` / `removeById`:离房;**仅观战(`hang`)锁座时也可离房**;**已入座**须 `seats_open`(**房主需业务先 `setOwner` 再 remove**)
152
240
  - `initialRoles`:定庄 + 盲位并 **lockSeats**;`rotateRoles`:局间移庄,**不** lock(下一手前由业务 `lockSeats`)
153
- - `lockSeats` / `unlockSeats`:显式锁/解锁(`Texas.reset()` 会在一手收尾后 `unlockSeats`)
154
- - `setOwner` / `setOwnerById` / `getBaseInfo` / `getPlayerById` / `getPlayersBySeatStatus` 等
241
+ - `lockSeats` / `unlockSeats`:也可经 `texas.lockSeats()` / `texas.unlockSeats()`
242
+ - `setOwner` / `setOwnerById` / `getPlayerById` / `getPlayersBySeatStatus` 等
155
243
 
156
244
  ---
157
245
 
@@ -159,18 +247,26 @@ texas.reset() // pool + dealer + controller 清理,controller → idle,并 u
159
247
 
160
248
  ```ts
161
249
  Texas.configureEngine({
162
- // trace、仿真开关等,见 TexasEngineGlobalOptions
250
+ simulation: {
251
+ // 单测/集成脚本:如 immediateDefaultActionOnTurn、allowSingleSeatedPlayer 等
252
+ }
163
253
  })
164
- Texas.resetEngineContext()
254
+ Texas.resetEngineContext() // jest.setup 已 beforeEach 调用
165
255
  ```
166
256
 
167
- 导出中还包含牌型/阶段/行动枚举与工具函数(如 `formatterPoke`、`StageEnum`、`ActionTypeEnum`、`isFatalTexasErrorCode` 等),便于与业务错误分级、持久化字段对齐。
257
+ 导出中还包含牌型/阶段/行动枚举、读模型 reducer(`reduceCommunityBoardFromDomainEvents` 等)、`createStandardDeckPokes`、`rankSignatureToDisplayGroups`、`isFatalTexasErrorCode`、`texasErrorCategory` 等。
168
258
 
169
259
  ---
170
260
 
171
261
  ## 更多文档
172
262
 
173
- - 架构与事件化演进思路:`docs/architecture-events-orchestration.md`、`docs/roadmap-command-event-interpreter.md`
263
+ | 文档 | 内容 |
264
+ | -------------------------------------------------------------------------------------------------------- | ------------------------------------ |
265
+ | [CORE_API.md](./CORE_API.md) | 对外 API 与不变量摘要 |
266
+ | [docs/integration-core-wish-event-flow.md](./docs/integration-core-wish-event-flow.md) | 与参考服务端的整局事件流、节拍、落库 |
267
+ | [docs/refactor-maintainer-reference.md](./docs/refactor-maintainer-reference.md) | 队列语义与维护速查 |
268
+ | [docs/replay-app-server-core-coordination.md](./docs/replay-app-server-core-coordination.md) | 回放磁带与 App/Server 配合 |
269
+ | [docs/How to refactor to be side-effect-free/](./docs/How%20to%20refactor%20to%20be%20side-effect-free/) | 事件化架构与路线图 |
174
270
 
175
271
  ---
176
272
 
@@ -620,4 +716,12 @@ reject modulo bias
620
716
  贴盲与 game start 原子化
621
717
 
622
718
  ## 1.4.29
623
- 修复post bb玩家过牌导致的异常
719
+
720
+ 修复 post bb 玩家过牌导致的异常
721
+
722
+ ## 1.4.30
723
+
724
+ 同步 README 接入文档
725
+
726
+ ## 1.4.31
727
+ 增加跑马摊牌事件
@@ -423,6 +423,7 @@ var Controller = /*#__PURE__*/function () {
423
423
  if (this.shouldShowDown()) {
424
424
  if (_classPrivateFieldGet(_hand, this).stage !== _stage.StageEnum.RIVER) {
425
425
  _classPrivateFieldSet(_runoutMode, this, true);
426
+ _assertClassBrand(_Controller_brand, this, _emitRunoutHandsRevealed).call(this);
426
427
  var from = _classPrivateFieldGet(_hand, this).stage;
427
428
  // 计算pending几次跑马
428
429
  while (from !== _stage.StageEnum.RIVER) {
@@ -619,6 +620,21 @@ function _everyPlayerNonActionableForRound() {
619
620
  return !pl.actionable();
620
621
  });
621
622
  }
623
+ /** 进入跑马路:对仍在局内的玩家同步亮底牌(在 `stage_advance` 入队前)。 */
624
+ function _emitRunoutHandsRevealed() {
625
+ var revealedHands = _classPrivateFieldGet(_dealer, this).getPlayersStillInGame().map(function (player) {
626
+ return {
627
+ userId: player.getUserInfo().id,
628
+ handPokes: player.getHandPokes()
629
+ };
630
+ });
631
+ _classPrivateFieldGet(_handEvents, this).push({
632
+ type: 'RunoutHandsRevealed',
633
+ payload: _objectSpread(_objectSpread({}, _assertClassBrand(_Controller_brand, this, _eventMeta).call(this)), {}, {
634
+ revealedHands: revealedHands
635
+ })
636
+ });
637
+ }
622
638
  /** 河牌摊牌:`settle` + `end` + `HandEnded(showdown)`(与跑马路最后一跳共用)。 */
623
639
  function _emitShowdownHandEnded() {
624
640
  _assertClassBrand(_Controller_brand, this, _settle).call(this);
@@ -14,6 +14,7 @@ exports.reduceLastHoleCardsDealtFromDomainEvents = reduceLastHoleCardsDealtFromD
14
14
  exports.reduceLastPostedBigBlindFromDomainEvents = reduceLastPostedBigBlindFromDomainEvents;
15
15
  exports.reduceLastPotAwardedFromDomainEvents = reduceLastPotAwardedFromDomainEvents;
16
16
  exports.reduceLastRolesAssignedFromDomainEvents = reduceLastRolesAssignedFromDomainEvents;
17
+ exports.reduceLastRunoutHandsRevealedFromDomainEvents = reduceLastRunoutHandsRevealedFromDomainEvents;
17
18
  exports.reduceLastStageAdvancedFromDomainEvents = reduceLastStageAdvancedFromDomainEvents;
18
19
  exports.reduceLastTurnOfferedFromDomainEvents = reduceLastTurnOfferedFromDomainEvents;
19
20
  exports.reducePlayerActedTrailFromDomainEvents = reducePlayerActedTrailFromDomainEvents;
@@ -323,14 +324,45 @@ function reduceLastStageAdvancedFromDomainEvents(events) {
323
324
  return last;
324
325
  }
325
326
 
326
- /** 磁带中首条 `HandStarted`(本手锚点)。 */
327
+ /** 本批中**最后一条** `RunoutHandsRevealed`(跑马摊牌;每手至多一条)。 */
327
328
 
328
- function reduceFirstHandStartedFromDomainEvents(events) {
329
+ function reduceLastRunoutHandsRevealedFromDomainEvents(events) {
330
+ var last = null;
329
331
  var _iterator11 = _createForOfIteratorHelper(events),
330
332
  _step11;
331
333
  try {
332
334
  for (_iterator11.s(); !(_step11 = _iterator11.n()).done;) {
333
335
  var e = _step11.value;
336
+ if (e.type === 'RunoutHandsRevealed') {
337
+ var p = e.payload;
338
+ last = {
339
+ handId: p.handId,
340
+ seq: p.seq,
341
+ revealedHands: p.revealedHands.map(function (row) {
342
+ return {
343
+ userId: row.userId,
344
+ handPokes: row.handPokes
345
+ };
346
+ })
347
+ };
348
+ }
349
+ }
350
+ } catch (err) {
351
+ _iterator11.e(err);
352
+ } finally {
353
+ _iterator11.f();
354
+ }
355
+ return last;
356
+ }
357
+
358
+ /** 磁带中首条 `HandStarted`(本手锚点)。 */
359
+
360
+ function reduceFirstHandStartedFromDomainEvents(events) {
361
+ var _iterator12 = _createForOfIteratorHelper(events),
362
+ _step12;
363
+ try {
364
+ for (_iterator12.s(); !(_step12 = _iterator12.n()).done;) {
365
+ var e = _step12.value;
334
366
  if (e.type === 'HandStarted') {
335
367
  return {
336
368
  handId: e.payload.handId,
@@ -339,9 +371,9 @@ function reduceFirstHandStartedFromDomainEvents(events) {
339
371
  }
340
372
  }
341
373
  } catch (err) {
342
- _iterator11.e(err);
374
+ _iterator12.e(err);
343
375
  } finally {
344
- _iterator11.f();
376
+ _iterator12.f();
345
377
  }
346
378
  return null;
347
379
  }
@@ -356,11 +388,11 @@ function reduceHandIdFromFirstHandStarted(events) {
356
388
 
357
389
  function reduceTurnEndedTrailFromDomainEvents(events) {
358
390
  var rows = [];
359
- var _iterator12 = _createForOfIteratorHelper(events),
360
- _step12;
391
+ var _iterator13 = _createForOfIteratorHelper(events),
392
+ _step13;
361
393
  try {
362
- for (_iterator12.s(); !(_step12 = _iterator12.n()).done;) {
363
- var e = _step12.value;
394
+ for (_iterator13.s(); !(_step13 = _iterator13.n()).done;) {
395
+ var e = _step13.value;
364
396
  if (e.type === 'TurnEnded') {
365
397
  var _e$payload4 = e.payload,
366
398
  seq = _e$payload4.seq,
@@ -374,9 +406,9 @@ function reduceTurnEndedTrailFromDomainEvents(events) {
374
406
  }
375
407
  }
376
408
  } catch (err) {
377
- _iterator12.e(err);
409
+ _iterator13.e(err);
378
410
  } finally {
379
- _iterator12.f();
411
+ _iterator13.f();
380
412
  }
381
413
  rows.sort(function (a, b) {
382
414
  return a.seq - b.seq;
@@ -388,11 +420,11 @@ function reduceTurnEndedTrailFromDomainEvents(events) {
388
420
 
389
421
  function reduceLastPostedBigBlindFromDomainEvents(events) {
390
422
  var last = null;
391
- var _iterator13 = _createForOfIteratorHelper(events),
392
- _step13;
423
+ var _iterator14 = _createForOfIteratorHelper(events),
424
+ _step14;
393
425
  try {
394
- for (_iterator13.s(); !(_step13 = _iterator13.n()).done;) {
395
- var e = _step13.value;
426
+ for (_iterator14.s(); !(_step14 = _iterator14.n()).done;) {
427
+ var e = _step14.value;
396
428
  if (e.type === 'PostedJoiningBigBlinds') {
397
429
  var posts = e.payload.posts;
398
430
  if (posts.length === 0) continue;
@@ -408,9 +440,9 @@ function reduceLastPostedBigBlindFromDomainEvents(events) {
408
440
  }
409
441
  }
410
442
  } catch (err) {
411
- _iterator13.e(err);
443
+ _iterator14.e(err);
412
444
  } finally {
413
- _iterator13.f();
445
+ _iterator14.f();
414
446
  }
415
447
  return last;
416
448
  }
@@ -419,11 +451,11 @@ function reduceLastPostedBigBlindFromDomainEvents(events) {
419
451
 
420
452
  function reduceLastRolesAssignedFromDomainEvents(events) {
421
453
  var last = null;
422
- var _iterator14 = _createForOfIteratorHelper(events),
423
- _step14;
454
+ var _iterator15 = _createForOfIteratorHelper(events),
455
+ _step15;
424
456
  try {
425
- for (_iterator14.s(); !(_step14 = _iterator14.n()).done;) {
426
- var e = _step14.value;
457
+ for (_iterator15.s(); !(_step15 = _iterator15.n()).done;) {
458
+ var e = _step15.value;
427
459
  if (e.type === 'RolesAssigned') {
428
460
  var p = e.payload;
429
461
  last = {
@@ -441,9 +473,9 @@ function reduceLastRolesAssignedFromDomainEvents(events) {
441
473
  }
442
474
  }
443
475
  } catch (err) {
444
- _iterator14.e(err);
476
+ _iterator15.e(err);
445
477
  } finally {
446
- _iterator14.f();
478
+ _iterator15.f();
447
479
  }
448
480
  return last;
449
481
  }
@@ -452,11 +484,11 @@ function reduceLastRolesAssignedFromDomainEvents(events) {
452
484
 
453
485
  function reduceLastHoleCardsDealtFromDomainEvents(events) {
454
486
  var last = null;
455
- var _iterator15 = _createForOfIteratorHelper(events),
456
- _step15;
487
+ var _iterator16 = _createForOfIteratorHelper(events),
488
+ _step16;
457
489
  try {
458
- for (_iterator15.s(); !(_step15 = _iterator15.n()).done;) {
459
- var e = _step15.value;
490
+ for (_iterator16.s(); !(_step16 = _iterator16.n()).done;) {
491
+ var e = _step16.value;
460
492
  if (e.type === 'HoleCardsDealt') {
461
493
  var _e$payload5 = e.payload,
462
494
  handId = _e$payload5.handId,
@@ -477,9 +509,9 @@ function reduceLastHoleCardsDealtFromDomainEvents(events) {
477
509
  }
478
510
  }
479
511
  } catch (err) {
480
- _iterator15.e(err);
512
+ _iterator16.e(err);
481
513
  } finally {
482
- _iterator15.f();
514
+ _iterator16.f();
483
515
  }
484
516
  return last;
485
517
  }
@@ -201,6 +201,12 @@ Object.defineProperty(exports, "reduceLastRolesAssignedFromDomainEvents", {
201
201
  return _domainEventReadModel.reduceLastRolesAssignedFromDomainEvents;
202
202
  }
203
203
  });
204
+ Object.defineProperty(exports, "reduceLastRunoutHandsRevealedFromDomainEvents", {
205
+ enumerable: true,
206
+ get: function get() {
207
+ return _domainEventReadModel.reduceLastRunoutHandsRevealedFromDomainEvents;
208
+ }
209
+ });
204
210
  Object.defineProperty(exports, "reduceLastStageAdvancedFromDomainEvents", {
205
211
  enumerable: true,
206
212
  get: function get() {
package/dist/index.js CHANGED
@@ -71,6 +71,7 @@ var _exportNames = {
71
71
  reduceLastPotAwardedFromDomainEvents: true,
72
72
  reduceLastBlindsPostedFromDomainEvents: true,
73
73
  reduceLastStageAdvancedFromDomainEvents: true,
74
+ reduceLastRunoutHandsRevealedFromDomainEvents: true,
74
75
  reduceFirstHandStartedFromDomainEvents: true,
75
76
  reduceHandIdFromFirstHandStarted: true,
76
77
  reduceTurnEndedTrailFromDomainEvents: true,
@@ -604,6 +605,12 @@ Object.defineProperty(exports, "reduceLastRolesAssignedFromDomainEvents", {
604
605
  return _engine.reduceLastRolesAssignedFromDomainEvents;
605
606
  }
606
607
  });
608
+ Object.defineProperty(exports, "reduceLastRunoutHandsRevealedFromDomainEvents", {
609
+ enumerable: true,
610
+ get: function get() {
611
+ return _engine.reduceLastRunoutHandsRevealedFromDomainEvents;
612
+ }
613
+ });
607
614
  Object.defineProperty(exports, "reduceLastStageAdvancedFromDomainEvents", {
608
615
  enumerable: true,
609
616
  get: function get() {
@@ -19,6 +19,7 @@ function projectCompositeReadModel(events) {
19
19
  pot: (0, _domainEventReadModel.reducePotFromDomainEvents)(events),
20
20
  communityBoard: (0, _domainEventReadModel.reduceCommunityBoardFromDomainEvents)(events),
21
21
  lastStageAdvanced: (0, _domainEventReadModel.reduceLastStageAdvancedFromDomainEvents)(events),
22
+ lastRunoutHandsRevealed: (0, _domainEventReadModel.reduceLastRunoutHandsRevealedFromDomainEvents)(events),
22
23
  lastTurnOffered: (0, _domainEventReadModel.reduceLastTurnOfferedFromDomainEvents)(events),
23
24
  playerActedTrail: (0, _domainEventReadModel.reducePlayerActedTrailFromDomainEvents)(events),
24
25
  turnEndedTrail: (0, _domainEventReadModel.reduceTurnEndedTrailFromDomainEvents)(events),
@@ -0,0 +1,194 @@
1
+ "use strict";
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); }
4
+ var fs = _interopRequireWildcard(require("node:fs"));
5
+ var path = _interopRequireWildcard(require("node:path"));
6
+ var _Texas = _interopRequireDefault(require("./Texas"));
7
+ var _core = require("./Deck/core");
8
+ var _constant = require("./Player/constant");
9
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
10
+ 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); }
11
+ 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; }
12
+ function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
13
+ 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."); }
14
+ 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; } }
15
+ function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
16
+ function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
17
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } /**
18
+ * 模拟 wish 业务层发牌流程(5 人 × 50 局),输出 Markdown 到仓库根目录。
19
+ *
20
+ * 首局(useCase):setPlayerRoles('initial') → dealCards()
21
+ * 后续局(eventBinder + HandEnded):unlockSeats → rotateRolesForNewHand →
22
+ * onLock: reset + lockSeats → setPlayerRoles('rearrange') → dealCards()
23
+ */
24
+ var PLAYER_COUNT = 5;
25
+ var DEAL_COUNT = 50;
26
+ var OUTPUT_FILE = path.join(__dirname, '..', 'DEAL_SIMULATION_5P_50H.md');
27
+ function fmtOne(poke) {
28
+ return (0, _core.formatterPoke)([poke]);
29
+ }
30
+ function fmt(pokes) {
31
+ return pokes.length ? (0, _core.formatterPoke)(_toConsumableArray(pokes)) : '—';
32
+ }
33
+ function buildDealSequenceLabels(shuffledDeck, playerCount) {
34
+ var deck = _toConsumableArray(shuffledDeck);
35
+ var labels = [];
36
+ var idx = 0;
37
+ for (var round = 0; round < 2; round++) {
38
+ for (var seat = 0; seat < playerCount; seat++) {
39
+ labels.push("#".concat(idx + 1, " \u624B\u724C \u5EA7\u4F4D").concat(seat + 1, " \u7B2C").concat(round + 1, "\u5F20 \u2192 ").concat(fmt([deck[idx]])));
40
+ idx++;
41
+ }
42
+ }
43
+ labels.push("#".concat(idx + 1, " \u70E7\u724C(flop\u524D) \u2192 ").concat(fmt([deck[idx]])));
44
+ idx++;
45
+ for (var i = 0; i < 3; i++) {
46
+ labels.push("#".concat(idx + 1, " \u7FFB\u724C").concat(i + 1, " \u2192 ").concat(fmt([deck[idx]])));
47
+ idx++;
48
+ }
49
+ labels.push("#".concat(idx + 1, " \u70E7\u724C(turn\u524D) \u2192 ").concat(fmt([deck[idx]])));
50
+ idx++;
51
+ labels.push("#".concat(idx + 1, " \u8F6C\u724C \u2192 ").concat(fmt([deck[idx]])));
52
+ idx++;
53
+ labels.push("#".concat(idx + 1, " \u70E7\u724C(river\u524D) \u2192 ").concat(fmt([deck[idx]])));
54
+ idx++;
55
+ labels.push("#".concat(idx + 1, " \u6CB3\u724C \u2192 ").concat(fmt([deck[idx]])));
56
+ idx++;
57
+ while (idx < deck.length) {
58
+ labels.push("#".concat(idx + 1, " \u672A\u53D1\u5269\u4F59 \u2192 ").concat(fmt([deck[idx]])));
59
+ idx++;
60
+ }
61
+ return labels;
62
+ }
63
+ function setupTexas() {
64
+ var texas = new _Texas.default({
65
+ lowestBetAmount: 20,
66
+ maximumCountOfPlayers: PLAYER_COUNT,
67
+ initialChips: 2000,
68
+ user: {
69
+ id: 1,
70
+ name: 'P1'
71
+ }
72
+ });
73
+ var players = [texas.room.owner, texas.createPlayer({
74
+ id: 2,
75
+ name: 'P2'
76
+ }), texas.createPlayer({
77
+ id: 3,
78
+ name: 'P3'
79
+ }), texas.createPlayer({
80
+ id: 4,
81
+ name: 'P4'
82
+ }), texas.createPlayer({
83
+ id: 5,
84
+ name: 'P5'
85
+ })];
86
+ for (var _i = 0, _players = players; _i < _players.length; _i++) {
87
+ var p = _players[_i];
88
+ if (p !== texas.room.owner) {
89
+ texas.room.join(p);
90
+ }
91
+ texas.room.seat(p);
92
+ }
93
+ return texas;
94
+ }
95
+ function runFirstHandDeal(texas) {
96
+ texas.setPlayerRoles('initial');
97
+ texas.dealCards();
98
+ }
99
+ function runNextHandDeal(texas) {
100
+ texas.unlockSeats();
101
+ texas.rotateRolesForNewHand();
102
+ texas.reset();
103
+ texas.lockSeats();
104
+ texas.setPlayerRoles('rearrange');
105
+ texas.dealCards();
106
+ }
107
+ function main() {
108
+ var texas = setupTexas();
109
+ var sections = [];
110
+ sections.push('# 5 人桌 × 50 次发牌模拟');
111
+ sections.push('');
112
+ sections.push('> 生成时间:' + new Date().toISOString());
113
+ sections.push('> 引擎:`texas-poker-core` `Deck#dealCards`(每次发牌前 Fisher–Yates 洗牌)');
114
+ sections.push('');
115
+ sections.push('## 业务层发牌流程(wish_mono_server)');
116
+ sections.push('');
117
+ sections.push('### 首局(`useCase.ts` → `#runStartFlow`)');
118
+ sections.push('');
119
+ sections.push('1. `texas.setPlayerRoles()`(默认 `initial`:定庄 + 锁座)');
120
+ sections.push('2. `texas.dealCards()` → `HoleCardsDealt`');
121
+ sections.push('3. `texas.start()`(本模拟仅记录发牌,不 `start`)');
122
+ sections.push('');
123
+ sections.push('### 后续局(`eventBinder.ts` 局间钩子 + `HandEnded` 收尾)');
124
+ sections.push('');
125
+ sections.push('1. 上一手结束:`texas.unlockSeats()` → `texas.rotateRolesForNewHand()`');
126
+ sections.push('2. 倒计时 lock:`texas.reset()` → `texas.lockSeats()`');
127
+ sections.push("3. `onAssignRoles`:`texas.setPlayerRoles('rearrange')`");
128
+ sections.push('4. `onDeal`:`texas.dealCards()`');
129
+ sections.push('5. `onStart`:`texas.startPreflopWithJoiningBigBlinds(...)`(本模拟省略)');
130
+ sections.push('');
131
+ sections.push('---');
132
+ sections.push('');
133
+ var _loop = function _loop() {
134
+ if (hand === 1) {
135
+ runFirstHandDeal(texas);
136
+ } else {
137
+ runNextHandDeal(texas);
138
+ }
139
+ var _texas$dealer$getPoke = texas.dealer.getPokes(),
140
+ commonPokes = _texas$dealer$getPoke.commonPokes,
141
+ handPokes = _texas$dealer$getPoke.handPokes;
142
+ var shuffledDeck = _toConsumableArray(texas.dealer.deck.getCards());
143
+ var playersInOrder = texas.dealer.getPlayersByActionSequence();
144
+ var dealPath = buildDealSequenceLabels(shuffledDeck, PLAYER_COUNT);
145
+ sections.push("## \u7B2C ".concat(hand, " \u6B21\u53D1\u724C").concat(hand === 1 ? '(首局 · initial)' : '(后续局 · rearrange)'));
146
+ sections.push('');
147
+ sections.push('### 1. 公牌(5 张,含烧牌后亮出的 flop/turn/river)');
148
+ sections.push('');
149
+ sections.push('```');
150
+ sections.push(fmt(commonPokes));
151
+ sections.push('```');
152
+ sections.push('');
153
+ sections.push('### 2. 各玩家手牌(按行动序,从庄家下家起)');
154
+ sections.push('');
155
+ sections.push('| 座位序 | userId | 昵称 | 角色 | 手牌 |');
156
+ sections.push('| ------ | ------ | ---- | ---- | ---- |');
157
+ playersInOrder.forEach(function (player, seatIdx) {
158
+ var _roleMap$get, _handPokes$seatIdx;
159
+ var _player$getUserInfo = player.getUserInfo(),
160
+ id = _player$getUserInfo.id,
161
+ name = _player$getUserInfo.name;
162
+ var role = player.getRole();
163
+ var roleLabel = role ? (_roleMap$get = _constant.roleMap.get(role)) !== null && _roleMap$get !== void 0 ? _roleMap$get : role : '—';
164
+ var hole = (_handPokes$seatIdx = handPokes[seatIdx]) !== null && _handPokes$seatIdx !== void 0 ? _handPokes$seatIdx : [];
165
+ sections.push("| ".concat(seatIdx + 1, " | ").concat(id, " | ").concat(name, " | ").concat(roleLabel, " | ").concat(fmt(hole), " |"));
166
+ });
167
+ sections.push('');
168
+ sections.push('### 3. 全部 52 张底牌(本次洗牌后牌堆顺序,自顶向下发)');
169
+ sections.push('');
170
+ sections.push('```');
171
+ sections.push(shuffledDeck.map(function (p, i) {
172
+ return "".concat(String(i + 1).padStart(2, ' '), ". ").concat(fmtOne(p), " (").concat(p, ")");
173
+ }).join('\n'));
174
+ sections.push('```');
175
+ sections.push('');
176
+ sections.push('<details>');
177
+ sections.push('<summary>发牌路径标注(含烧牌)</summary>');
178
+ sections.push('');
179
+ sections.push('```');
180
+ sections.push(dealPath.join('\n'));
181
+ sections.push('```');
182
+ sections.push('');
183
+ sections.push('</details>');
184
+ sections.push('');
185
+ sections.push('---');
186
+ sections.push('');
187
+ };
188
+ for (var hand = 1; hand <= DEAL_COUNT; hand++) {
189
+ _loop();
190
+ }
191
+ fs.writeFileSync(OUTPUT_FILE, sections.join('\n'), 'utf8');
192
+ console.log("Wrote ".concat(DEAL_COUNT, " deals to ").concat(OUTPUT_FILE));
193
+ }
194
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "texas-poker-core",
3
- "version": "1.4.29",
3
+ "version": "1.4.31",
4
4
  "description": "德州扑克核心功能",
5
5
  "main": "dist/index.js",
6
6
  "types": "types/index.d.ts",
@@ -15,6 +15,11 @@ export type HandEventMeta = {
15
15
  handId: string;
16
16
  seq: number;
17
17
  };
18
+ /** 跑马开始时对仍在摊牌中的玩家亮出的底牌(不含 rank;牌力由客户端用 core 自算)。 */
19
+ export type RunoutRevealedHand = {
20
+ userId: number;
21
+ handPokes: Poke[];
22
+ };
18
23
  /**
19
24
  * 本手领域事件(无业务 callback;由 {@link Texas#dispatchCommand} 等同步返回,或 {@link Texas#drainDomainEvents} / Controller 缓冲取出)。
20
25
  * 含 **分配角色 → 发洞牌 → 贴盲与街道** 的完整磁带,便于「只重放某一手」时按 `handId` 过滤即可。
@@ -95,6 +100,16 @@ export type HandDomainEvent = {
95
100
  pokesRevealedThisStep: Poke[];
96
101
  advanceKind: 'betting_round_complete' | 'runout_reveal';
97
102
  };
103
+ }
104
+ /**
105
+ * 进入跑马路时同步发出(在首条 `StageAdvanced(runout_reveal)` 入队之前)。
106
+ * 业务层映射为 WS `runout-hands-revealed`,不受 `pendingFlowOps` pacing 影响。
107
+ */
108
+ | {
109
+ type: 'RunoutHandsRevealed';
110
+ payload: HandEventMeta & {
111
+ revealedHands: RunoutRevealedHand[];
112
+ };
98
113
  } | {
99
114
  type: 'TurnOffered';
100
115
  payload: HandEventMeta & {
@@ -87,6 +87,16 @@ export type StageAdvancedReadModel = Readonly<{
87
87
  advanceKind: 'betting_round_complete' | 'runout_reveal';
88
88
  }>;
89
89
  export declare function reduceLastStageAdvancedFromDomainEvents(events: readonly TexasDomainEvent[]): StageAdvancedReadModel | null;
90
+ /** 本批中**最后一条** `RunoutHandsRevealed`(跑马摊牌;每手至多一条)。 */
91
+ export type RunoutHandsRevealedReadModel = Readonly<{
92
+ handId: string;
93
+ seq: number;
94
+ revealedHands: ReadonlyArray<Readonly<{
95
+ userId: number;
96
+ handPokes: readonly Poke[];
97
+ }>>;
98
+ }>;
99
+ export declare function reduceLastRunoutHandsRevealedFromDomainEvents(events: readonly TexasDomainEvent[]): RunoutHandsRevealedReadModel | null;
90
100
  /** 磁带中首条 `HandStarted`(本手锚点)。 */
91
101
  export type HandStartedReadModel = Readonly<{
92
102
  handId: string;
@@ -9,7 +9,7 @@ export type { ApplyTableCommandResult } from './applyTableCommand';
9
9
  export { pendingFlowOpsAllowVoluntaryDispatch, peekPendingFlowOp, simulateDequeuePendingHeadIfMatches } from './pendingFlowReadModel';
10
10
  export { captureSeatUserIdsInActionOrder } from './dealerRingReadModel';
11
11
  export { applyTableCommand, applyTableCommandThenFlushAllPendingFlowOps } from './applyTableCommand';
12
- export type { BlindsPostedReadModel, HandEndedReadModel, HandStartedReadModel, HoleCardsDealtReadModel, PlayerActedEntry, PostedBigBlindReadModel, PotAwardedReadModel, PotContributionReadModel, RolesAssignedReadModel, StageAdvancedReadModel, TurnEndedEntry, TurnOfferReadModel } from './domainEventReadModel';
13
- export { flatConcatDomainEvents, filterDomainEventsByHandId, reducePotFromDomainEvents, reduceLastTurnOfferedFromDomainEvents, reducePlayerActedTrailFromDomainEvents, reduceCommunityBoardFromDomainEvents, reduceLastHandEndedFromDomainEvents, reduceLastPotAwardedFromDomainEvents, reduceLastBlindsPostedFromDomainEvents, reduceLastStageAdvancedFromDomainEvents, reduceFirstHandStartedFromDomainEvents, reduceHandIdFromFirstHandStarted, reduceTurnEndedTrailFromDomainEvents, reduceLastPostedBigBlindFromDomainEvents, reduceLastRolesAssignedFromDomainEvents, reduceLastHoleCardsDealtFromDomainEvents } from './domainEventReadModel';
12
+ export type { BlindsPostedReadModel, HandEndedReadModel, HandStartedReadModel, HoleCardsDealtReadModel, PlayerActedEntry, PostedBigBlindReadModel, PotAwardedReadModel, PotContributionReadModel, RolesAssignedReadModel, StageAdvancedReadModel, RunoutHandsRevealedReadModel, TurnEndedEntry, TurnOfferReadModel } from './domainEventReadModel';
13
+ export { flatConcatDomainEvents, filterDomainEventsByHandId, reducePotFromDomainEvents, reduceLastTurnOfferedFromDomainEvents, reducePlayerActedTrailFromDomainEvents, reduceCommunityBoardFromDomainEvents, reduceLastHandEndedFromDomainEvents, reduceLastPotAwardedFromDomainEvents, reduceLastBlindsPostedFromDomainEvents, reduceLastStageAdvancedFromDomainEvents, reduceLastRunoutHandsRevealedFromDomainEvents, reduceFirstHandStartedFromDomainEvents, reduceHandIdFromFirstHandStarted, reduceTurnEndedTrailFromDomainEvents, reduceLastPostedBigBlindFromDomainEvents, reduceLastRolesAssignedFromDomainEvents, reduceLastHoleCardsDealtFromDomainEvents } from './domainEventReadModel';
14
14
  export type { HandReduceProjection, FoldOrCheckTableCommand, VoluntaryTableCommand } from './handReducer';
15
15
  export { captureHandReduceProjection, applyVoluntaryTableCommand, applyFoldOrCheckCommand, isVoluntaryTableCommand } from './handReducer';
package/types/index.d.ts CHANGED
@@ -23,12 +23,12 @@ type TexasErrorCodeLegacy, CurrentHand };
23
23
  export type { TexasDomainEvent, HandDomainEvent, CreateRoomInputArgs } from './Texas';
24
24
  export type { TableCommand } from './domain/tableCommand';
25
25
  export { parseTableCommandFromJson, parseTableCommandFromUnknown } from './domain/tableCommandParse';
26
- export type { TurnEndedReason, HandEventMeta } from './domain/handDomainEvents';
26
+ export type { TurnEndedReason, HandEventMeta, RunoutRevealedHand } from './domain/handDomainEvents';
27
27
  export type { OrchestrationCtx, DomainEventHandler } from './orchestration/interpret';
28
28
  export { interpret } from './orchestration/interpret';
29
29
  export { createDefaultPipelineHandlers, defaultPipelineOrdered } from './orchestration/defaultPipeline';
30
- export { dispatchCommandAndInterpret, interpretTableEvents, flushAllPendingFlowOpsAndInterpret, captureTableSnapshot, applyTableCommand, applyTableCommandThenFlushAllPendingFlowOps, flatConcatDomainEvents, filterDomainEventsByHandId, reducePotFromDomainEvents, reduceLastTurnOfferedFromDomainEvents, reducePlayerActedTrailFromDomainEvents, reduceCommunityBoardFromDomainEvents, reduceLastHandEndedFromDomainEvents, reduceLastPotAwardedFromDomainEvents, reduceLastBlindsPostedFromDomainEvents, reduceLastStageAdvancedFromDomainEvents, reduceFirstHandStartedFromDomainEvents, reduceHandIdFromFirstHandStarted, reduceTurnEndedTrailFromDomainEvents, reduceLastPostedBigBlindFromDomainEvents, reduceLastRolesAssignedFromDomainEvents, reduceLastHoleCardsDealtFromDomainEvents, captureHandReduceProjection, applyVoluntaryTableCommand, applyFoldOrCheckCommand, isVoluntaryTableCommand, pendingFlowOpsAllowVoluntaryDispatch, peekPendingFlowOp, simulateDequeuePendingHeadIfMatches, captureSeatUserIdsInActionOrder, CANONICAL_TABLE_SESSION_JSON_SCHEMA_VERSION, cloneTableSnapshot, parseCanonicalTableSessionFromJson, reduceCanonicalTableSession, TABLE_STATE_V1_SCHEMA_VERSION, assertTableMatchesStateV1Snapshot, freezeTableStateV1FromLive, reduceCanonicalSessionToTableStateV1, applyTableCommandWithStateV1 } from './engine';
31
- export type { TableSnapshot, TablePlayerSnapshot, BootstrapInstruction, CanonicalTableSession, CommandStepInstruction, ReduceCanonicalTableSessionResult, TableStateV1, ApplyTableCommandWithStateV1Result, ApplyTableCommandResult, BlindsPostedReadModel, HandEndedReadModel, HandStartedReadModel, HoleCardsDealtReadModel, PlayerActedEntry, PostedBigBlindReadModel, PotAwardedReadModel, PotContributionReadModel, RolesAssignedReadModel, StageAdvancedReadModel, TurnEndedEntry, TurnOfferReadModel, HandReduceProjection, FoldOrCheckTableCommand, VoluntaryTableCommand } from './engine';
30
+ export { dispatchCommandAndInterpret, interpretTableEvents, flushAllPendingFlowOpsAndInterpret, captureTableSnapshot, applyTableCommand, applyTableCommandThenFlushAllPendingFlowOps, flatConcatDomainEvents, filterDomainEventsByHandId, reducePotFromDomainEvents, reduceLastTurnOfferedFromDomainEvents, reducePlayerActedTrailFromDomainEvents, reduceCommunityBoardFromDomainEvents, reduceLastHandEndedFromDomainEvents, reduceLastPotAwardedFromDomainEvents, reduceLastBlindsPostedFromDomainEvents, reduceLastStageAdvancedFromDomainEvents, reduceLastRunoutHandsRevealedFromDomainEvents, reduceFirstHandStartedFromDomainEvents, reduceHandIdFromFirstHandStarted, reduceTurnEndedTrailFromDomainEvents, reduceLastPostedBigBlindFromDomainEvents, reduceLastRolesAssignedFromDomainEvents, reduceLastHoleCardsDealtFromDomainEvents, captureHandReduceProjection, applyVoluntaryTableCommand, applyFoldOrCheckCommand, isVoluntaryTableCommand, pendingFlowOpsAllowVoluntaryDispatch, peekPendingFlowOp, simulateDequeuePendingHeadIfMatches, captureSeatUserIdsInActionOrder, CANONICAL_TABLE_SESSION_JSON_SCHEMA_VERSION, cloneTableSnapshot, parseCanonicalTableSessionFromJson, reduceCanonicalTableSession, TABLE_STATE_V1_SCHEMA_VERSION, assertTableMatchesStateV1Snapshot, freezeTableStateV1FromLive, reduceCanonicalSessionToTableStateV1, applyTableCommandWithStateV1 } from './engine';
31
+ export type { TableSnapshot, TablePlayerSnapshot, BootstrapInstruction, CanonicalTableSession, CommandStepInstruction, ReduceCanonicalTableSessionResult, TableStateV1, ApplyTableCommandWithStateV1Result, ApplyTableCommandResult, BlindsPostedReadModel, HandEndedReadModel, HandStartedReadModel, HoleCardsDealtReadModel, PlayerActedEntry, PostedBigBlindReadModel, PotAwardedReadModel, PotContributionReadModel, RolesAssignedReadModel, StageAdvancedReadModel, RunoutHandsRevealedReadModel, TurnEndedEntry, TurnOfferReadModel, HandReduceProjection, FoldOrCheckTableCommand, VoluntaryTableCommand } from './engine';
32
32
  export type { PersistedDomainEventRow, DomainEventStore, DomainEventsCompositeReadModel, DomainEventTapeValidationIssue, VerifyCanonicalAgainstTapeResult, VerifyCanonicalAgainstTapeCompactResult, ReplayVerifyExitPolicy, VerifyCanonicalDiffContext, VerifyEventSignature } from './replay';
33
33
  export { toPersistedDomainEventRows, createInMemoryDomainEventStore, domainEventsFromPersistedRows, validatePersistedDomainEventRows, assertPersistedDomainEventRows, summarizePersistedDomainEventRows, verifyCanonicalSessionAgainstPersistedRows, toCompactVerifyCanonicalAgainstTapeResult, resolveReplayVerifyExitCode, createAppendDomainEventsHandler, projectCompositeReadModel } from './replay';
34
34
  export type { PreAction } from './gameContracts';
@@ -1,6 +1,6 @@
1
1
  import type { Poke } from '../Deck/constant';
2
2
  import type { TexasDomainEvent } from '../domain/handDomainEvents';
3
- import type { TurnEndedEntry, PlayerActedEntry, HandEndedReadModel, TurnOfferReadModel, PotAwardedReadModel, HandStartedReadModel, BlindsPostedReadModel, RolesAssignedReadModel, StageAdvancedReadModel, HoleCardsDealtReadModel, PostedBigBlindReadModel, PotContributionReadModel } from '../engine/domainEventReadModel';
3
+ import type { TurnEndedEntry, PlayerActedEntry, HandEndedReadModel, TurnOfferReadModel, PotAwardedReadModel, HandStartedReadModel, BlindsPostedReadModel, RolesAssignedReadModel, StageAdvancedReadModel, HoleCardsDealtReadModel, PostedBigBlindReadModel, PotContributionReadModel, RunoutHandsRevealedReadModel } from '../engine/domainEventReadModel';
4
4
  /**
5
5
  * 单条磁带上的**只读复合投影**(阶段 6 向 `reduce(apply)` 过渡的读侧聚合;**非**完整状态机)。
6
6
  * 供回放对拍、机器人读盘、接入方一条 API 取多视角。
@@ -13,6 +13,7 @@ export type DomainEventsCompositeReadModel = Readonly<{
13
13
  pot: PotContributionReadModel;
14
14
  communityBoard: readonly Poke[];
15
15
  lastStageAdvanced: StageAdvancedReadModel | null;
16
+ lastRunoutHandsRevealed: RunoutHandsRevealedReadModel | null;
16
17
  lastTurnOffered: TurnOfferReadModel | null;
17
18
  playerActedTrail: readonly PlayerActedEntry[];
18
19
  turnEndedTrail: readonly TurnEndedEntry[];
@@ -0,0 +1 @@
1
+ export {};