Combinations
斗地主牌型系统:13 种牌型枚举、牌型对象、检测器总入口、13 个独立 Detector、比较器、点数解析器与共享工具函数。
概述
斗地主牌型系统由以下模块构成:
| 模块 | 文件 | 说明 |
|---|---|---|
| 牌型枚举 | DoudizhuCombinationType.ts | 13 种牌型 + UNKNOWN 兜底 |
| 牌型对象 | DoudizhuCombination.ts | DoudizhuCombination 接口与 makeCombination / unknownCombination 工厂 |
| 检测器总入口 | DoudizhuCombinationDetector.ts | DETECTORS 列表、DoudizhuCombinationDetector 类、detectDoudizhuCombination / isBombFamily |
| 13 个 Detector | detectors/*.ts | 每个牌型一个纯函数 Card[] -> DoudizhuCombination | null |
| 比较器 | DoudizhuCombinationComparator.ts | "可压过"规则与 DoudizhuCompareResult 枚举 |
| 点数解析器 | DoudizhuRankResolver.ts | 实现 RankResolver 接口,供通用 CardQuery 复用 |
| 共享工具 | detectors/helpers.ts | rankCounts / findConsecutiveRun / leftoverAfter 等 |
DoudizhuCombinationType
enum DoudizhuCombinationType {
ROCKET = 'ROCKET',
BOMB = 'BOMB',
AIRPLANE_WITH_PAIR = 'AIRPLANE_WITH_PAIR',
AIRPLANE_WITH_SINGLE = 'AIRPLANE_WITH_SINGLE',
AIRPLANE = 'AIRPLANE',
FOUR_WITH_TWO = 'FOUR_WITH_TWO',
STRAIGHT = 'STRAIGHT',
CONSECUTIVE_PAIRS = 'CONSECUTIVE_PAIRS',
TRIPLE_WITH_PAIR = 'TRIPLE_WITH_PAIR',
TRIPLE_WITH_SINGLE = 'TRIPLE_WITH_SINGLE',
TRIPLE = 'TRIPLE',
PAIR = 'PAIR',
SINGLE = 'SINGLE',
UNKNOWN = 'UNKNOWN'
}全部斗地主牌型类型。声明顺序遵循检测器使用的检测优先级(最高优先级在前);UNKNOWN 留给不匹配任何规则的卡牌集合。
| 枚举值 | 牌型 | 说明 |
|---|---|---|
ROCKET | 火箭 / 王炸 | 小王 + 大王。压一切。 |
BOMB | 炸弹 | 四张同点(例如 3333)。压任意非 rocket 牌型。 |
AIRPLANE_WITH_PAIR | 飞机带对 | n 个连续三张 + n 个对子(n ≥ 2)。 |
AIRPLANE_WITH_SINGLE | 飞机带单 | n 个连续三张 + n 个单张(n ≥ 2)。 |
AIRPLANE | 飞机 | n 个连续三张(n ≥ 2)。 |
FOUR_WITH_TWO | 四带二 | 四张同点 + 2 张单牌,或四张同点 + 2 个对子。 |
STRAIGHT | 顺子 | 5+ 张连续单牌(仅 3..A;不含 2 和大小王)。 |
CONSECUTIVE_PAIRS | 连对 | 3+ 个连续对子(仅 3..A)。 |
TRIPLE_WITH_PAIR | 三带二 | 一个三张 + 一个对子(例如 333 55)。 |
TRIPLE_WITH_SINGLE | 三带一 | 一个三张 + 一个单张(例如 333 5)。 |
TRIPLE | 三张 | 三张同点(例如 333)。 |
PAIR | 对子 | 两张同点(例如 33)。 |
SINGLE | 单张 | 单张牌。 |
UNKNOWN | 未识别 | 未匹配任何有效斗地主牌型。 |
该枚举有意与核心 CombinationType(扑克)分离。斗地主的牌型形态不污染通用牌型系统。
combinationTypeName()
function combinationTypeName(t: DoudizhuCombinationType): string牌型类型的人类可读名称。当前实现为恒等函数(直接返回入参)。
DoudizhuCombination
interface DoudizhuCombination {
type: DoudizhuCombinationType
cards: Card[]
mainRank: number
length: number
chainLength?: number
attachments?: Card[]
metadata?: Record<string, unknown>
}在一组卡牌上识别出的斗地主牌型。
| 字段 | 类型 | 说明 |
|---|---|---|
type | DoudizhuCombinationType | 命中的斗地主牌型形态 |
cards | Card[] | 产生该牌型的完整卡牌集合 |
mainRank | number | 用于比较的主点数。对于链式牌型(STRAIGHT/CONSECUTIVE_PAIRS/AIRPLANE*)为链中的最小点数;对于 BOMB/FOUR_WITH_TWO 为四张的点数;对于 ROCKET 为 BIG_JOKER_POINT(17) |
length | number | 该牌型中的卡牌总数 |
chainLength? | number | undefined | 链式牌型:链单元数量(顺子 = 单张数量;连对 = 对子数量;飞机变体 = 三张数量)。非链式牌型为 undefined |
attachments? | Card[] | 附挂于三张/四张/飞机的附件牌(单张/对子)。纯牌型为 undefined |
metadata? | Record<string, unknown> | 自由结构,供检测器记录额外细节(例如附件拆分明细) |
makeCombination()
function makeCombination(
type: DoudizhuCombinationType,
cards: Card[],
mainRank: number,
opts?: { chainLength?: number; attachments?: Card[]; metadata?: Record<string, unknown> }
): DoudizhuCombination构造牌型对象。length 由 cards.length 推导;chainLength / attachments / metadata 仅在 opts 中提供时写入;attachments 为空数组时不写入(保持 undefined),以避免无意义字段。
参数
type: DoudizhuCombinationType— 牌型cards: Card[]— 构成该牌型的全部卡牌mainRank: number— 用于比较的主点数(语义见上表)opts?— 可选:chainLength(链长)、attachments(附件牌)、metadata(附加元数据)
返回值
DoudizhuCombination— 构造好的牌型对象
unknownCombination()
function unknownCombination(cards: Card[]): DoudizhuCombination构造无法识别的 UNKNOWN 牌型(mainRank=0)。用于检测器全部不匹配时的兜底。
参数
cards: Card[]— 未识别的卡牌集合
返回值
DoudizhuCombination—{ type: UNKNOWN, cards, mainRank: 0, length: cards.length }
DoudizhuCombinationDetector(检测器总入口)
来源:combinations/DoudizhuCombinationDetector.ts。
DETECTORS
const DETECTORS: ReadonlyArray<
(cards: Card[]) => DoudizhuCombination | null
>斗地主牌型检测器(Detector)的有序列表。顺序很重要:当一组牌可匹配多种形态时,首个匹配胜出。此顺序对应 Phase 5 规范中的检测优先级:
| 序号 | 检测器 | 牌型 |
|---|---|---|
| 1 | detectRocket | ROCKET |
| 2 | detectBomb | BOMB |
| 3 | detectAirplaneWithPair | AIRPLANE_WITH_PAIR |
| 4 | detectAirplaneWithSingle | AIRPLANE_WITH_SINGLE |
| 5 | detectAirplane | AIRPLANE |
| 6 | detectFourWithTwo | FOUR_WITH_TWO |
| 7 | detectStraight | STRAIGHT |
| 8 | detectConsecutivePairs | CONSECUTIVE_PAIRS |
| 9 | detectTripleWithPair | TRIPLE_WITH_PAIR |
| 10 | detectTripleWithSingle | TRIPLE_WITH_SINGLE |
| 11 | detectTriple | TRIPLE |
| 12 | detectPair | PAIR |
| 13 | detectSingle | SINGLE |
该列表对外导出,便于插件 / 测试检视或重排。每个条目都是纯函数 Card[] -> DoudizhuCombination | null。
DoudizhuCombinationDetector 类
class DoudizhuCombinationDetector {
constructor(
detectors?: ReadonlyArray<(cards: Card[]) => DoudizhuCombination | null>
)
detect(cards: readonly Card[]): DoudizhuCombination
}按优先级顺序对输入卡牌集合运行检测器列表,返回首个匹配结果。若无任何检测器匹配,返回 UNKNOWN 牌型。
该检测器有意设计为纯的、无状态的类(不耦合引擎)。其 API 与扑克的 CombinationDetector 对齐,下游代码(规则、测试)可对称地使用它们。
构造函数
constructor(
detectors?: ReadonlyArray<(cards: Card[]) => DoudizhuCombination | null>
)参数
detectors?— 自定义检测器列表。省略时使用默认DETECTORS列表。
detect()
detect(cards: readonly Card[]): DoudizhuCombination参数
cards: readonly Card[]— 待检测的卡牌
返回值
DoudizhuCombination— 识别到的牌型;无匹配时为UNKNOWN
行为
cards.length === 0→ 直接返回unknownCombination([]);- 否则按顺序运行
this.detectors中每个检测器,传入[...cards](复制以避免检测器污染原数组),首个返回非null的结果胜出; - 全部不匹配 → 返回
unknownCombination([...cards])。
detectDoudizhuCombination()
function detectDoudizhuCombination(cards: readonly Card[]): DoudizhuCombination便捷纯函数:使用默认检测器顺序从一组卡牌中检测牌型。无状态——可在任何位置安全调用。
示例
import {
detectDoudizhuCombination,
DoudizhuCombinationType
} from 'decklet/doudizhu'
import { createDoudizhuCards } from 'decklet/doudizhu'
const all = createDoudizhuCards()
const card3 = all.find((c) => c.id === 'ddz-♠-3')!
const card3h = all.find((c) => c.id === 'ddz-♥-3')!
const combo = detectDoudizhuCombination([card3, card3h])
console.log(combo.type === DoudizhuCombinationType.PAIR) // true
console.log(combo.mainRank) // 3
console.log(combo.length) // 2isBombFamily()
function isBombFamily(c: DoudizhuCombination): boolean判断牌型是否属于"炸弹系列"(Rocket 或 Bomb)。炸弹系列可压任意非炸弹系列牌型。
参数
c: DoudizhuCombination— 待判断的牌型
返回值
boolean— 为ROCKET或BOMB时返回true,否则false
13 个 Detector
所有 Detector 均为纯函数 (cards: Card[]) => DoudizhuCombination | null,位于 combinations/detectors/ 目录下。下文按 DETECTORS 列表的优先级顺序逐一介绍。
detectSingle(SINGLE)
import { detectSingle } from './detectors/SingleDetector.js'
function detectSingle(cards: Card[]): DoudizhuCombination | nullSINGLE(单张):恰好一张牌。任意一张牌(花色牌或王)都是合法的单张。
校验逻辑
cards.length !== 1→null- 否则 →
makeCombination(SINGLE, cards, pointOf(cards[0]))
示例
// 任意一张牌 → SINGLE,mainRank 为该牌点数
detectSingle([card3]) // { type: SINGLE, mainRank: 3, length: 1 }detectPair(PAIR)
import { detectPair } from './detectors/PairDetector.js'
function detectPair(cards: Card[]): DoudizhuCombination | nullPAIR(对子):两张同点数的牌。由于每张王都是唯一的(一副牌中只有一张小王和一张大王),两张王不能组成对子——只有两张同点数的花色牌才算合法对子。
校验逻辑
cards.length !== 2→nullpointOf(cards[0]) !== pointOf(cards[1])→null- 否则 →
makeCombination(PAIR, cards, pointOf(cards[0]))
示例
detectPair([card3s, card3h]) // PAIR,mainRank=3
detectPair([smallJoker, bigJoker]) // null(两张王不是 PAIR,会由 detectRocket 命中)detectTriple(TRIPLE)
import { detectTriple } from './detectors/TripleDetector.js'
function detectTriple(cards: Card[]): DoudizhuCombination | nullTRIPLE(三张):恰好三张同点数的牌(例如 333)。由于每张王都只有一张,王不能组成三张。
校验逻辑
cards.length !== 3→nullrankCounts(cards).length !== 1→null(必须只有一种点数)counts[0].count !== 3→null- 否则 →
makeCombination(TRIPLE, cards, counts[0].point)
detectStraight(STRAIGHT)
import { detectStraight } from './detectors/StraightDetector.js'
function detectStraight(cards: Card[]): DoudizhuCombination | nullSTRAIGHT(顺子):5+ 张连续可链接的单牌。可链接点数为 3..A(即点数 3..14)。2(15)和王(16/17)不能参与。
校验逻辑
cards.length < 5→nullrankCounts(cards).length !== cards.length→null(每张牌必须互异)- 任一
rc.count !== 1→null - 任一点数不在
[3, 14]→null - 任一相邻点数不连续(
counts[i].point !== counts[i-1].point + 1)→null - 否则 →
makeCombination(STRAIGHT, cards, counts[0].point, { chainLength: cards.length })
示例
34567 ✓ (长度 5)
10JQKA ✓ (长度 5)
23456 ✗ (2 不能链接)
JQKA2 ✗ (2 不能链接)
小王/大王 ✗ (王不能链接)detectConsecutivePairs(CONSECUTIVE_PAIRS)
import { detectConsecutivePairs } from './detectors/ConsecutivePairsDetector.js'
function detectConsecutivePairs(cards: Card[]): DoudizhuCombination | nullCONSECUTIVE_PAIRS(连对):3+ 个连续可链接的对子。每个点数必须恰好 count=2,且所有点数必须可链接(3..14)并连续。
校验逻辑
cards.length < 6 || cards.length % 2 !== 0→null- 任一
rc.count !== 2→null counts.length < 3→null- 任一点数不在
[3, 14]→null - 任一相邻点数不连续 →
null - 否则 →
makeCombination(CONSECUTIVE_PAIRS, cards, counts[0].point, { chainLength: counts.length })
示例
334455 ✓ (长度 3 对)
33445566 ✓ (长度 4 对)
QQKKAA22 ✗ (2 不能链接)detectAirplane(AIRPLANE)
import { detectAirplane } from './detectors/AirplaneDetector.js'
function detectAirplane(cards: Card[]): DoudizhuCombination | nullAIRPLANE(飞机):2+ 个连续可链接的三张(每个点数 count==3)。共 3 * N 张牌,其中 N >= 2。点数必须可链接(3..14)并连续。无附件。
校验逻辑
cards.length < 6 || cards.length % 3 !== 0→null- 任一
rc.count !== 3→null counts.length < 2→null- 任一点数不在
[3, 14]→null - 任一相邻点数不连续 →
null - 否则 →
makeCombination(AIRPLANE, cards, counts[0].point, { chainLength: counts.length })
示例
333444 ✓ (长度 2 三张)
333444555 ✓ (长度 3 三张)detectAirplaneWithSingle(AIRPLANE_WITH_SINGLE)
import { detectAirplaneWithSingle } from './detectors/AirplaneWithSingleDetector.js'
function detectAirplaneWithSingle(cards: Card[]): DoudizhuCombination | nullAIRPLANE_WITH_SINGLE(飞机带单):N 个连续可链接的三张 + N 个单张附件(N >= 2)。共 4 * N 张牌。
Phase 5 严格规则
依据规范:"attachment 可以来自其他点数"和"不允许拆飞机主体":
- 查找恰好 N 个连续可链接点数,每个点数
count恰好为 3(无余量——主体点数不可拆分); - 所有非主体牌的总数必须恰好为 N。每个非主体点数必须恰好贡献 1 张(即附件为来自 N 个互异非主体点数的 N 个单张)。若某非主体点数
count >= 2则会构成对子(另一种牌型),在此被拒绝。
校验逻辑
cards.length < 8 || cards.length % 4 !== 0→nullN = cards.length / 4,若N < 2→null- 在
eligible = counts.filter(rc => canChain(rc.point) && rc.count === 3)中查找恰好长度为N的连续点数序列(findRunOfLength); - 收集非主体牌:每个非主体点数必须
rc.count === 1,否则null; attachments.length !== N→null- 否则 →
makeCombination(AIRPLANE_WITH_SINGLE, cards, run[0], { chainLength: N, attachments })
示例
33344456 → 主体 3-4(N=2),非主体 5、6 各 count=1 → 2 个单张 ✓
3334445555 → 主体 3-4(N=2),但非主体点数 5 的 count=4
→ 剩余(4 张)!= N(2)→ 拒绝 ✗注意事项
findRunOfLength 仅匹配"恰好"长度为 targetLen 的连续点数序列——若存在更长的连续段则不匹配(这些牌应属于更大的飞机变体;Phase 5 中避免歧义拆分)。
detectAirplaneWithPair(AIRPLANE_WITH_PAIR)
import { detectAirplaneWithPair } from './detectors/AirplaneWithPairDetector.js'
function detectAirplaneWithPair(cards: Card[]): DoudizhuCombination | nullAIRPLANE_WITH_PAIR(飞机带对):N 个连续可链接的三张 + N 个对子附件(N >= 2)。共 5 * N 张牌。
Phase 5 严格规则
- 查找恰好 N 个连续可链接点数,每个点数
count恰好为 3(主体点数不可拆分); - 非主体牌总数必须恰好为
2*N。每个非主体点数必须恰好贡献 2 张(即附件为来自 N 个互异非主体点数的 N 个对子)。
校验逻辑
cards.length < 10 || cards.length % 5 !== 0→nullN = cards.length / 5,若N < 2→null- 同样用
findRunOfLength(eligible, N)查找主体; - 每个非主体点数必须
rc.count === 2,否则null; attachments.length !== 2 * N→null- 否则 →
makeCombination(AIRPLANE_WITH_PAIR, cards, run[0], { chainLength: N, attachments })
示例
3334445566 → 主体 3-4(N=2),非主体 5、6 各 count=2 → 2 个对子 ✓detectBomb(BOMB)
import { detectBomb } from './detectors/BombDetector.js'
function detectBomb(cards: Card[]): DoudizhuCombination | nullBOMB(炸弹):恰好四张同点数的牌(例如 3333、AAAA、2222)。压任意非 rocket 牌型。
校验逻辑
cards.length !== 4→nullrankCounts(cards).length !== 1→nullcounts[0].count !== 4→null- 否则 →
makeCombination(BOMB, cards, counts[0].point)
detectRocket(ROCKET)
import { detectRocket } from './detectors/RocketDetector.js'
function detectRocket(cards: Card[]): DoudizhuCombination | nullROCKET(火箭 / 王炸):小王 + 大王。压一切(包括炸弹)。一副牌中恰好只有一张小王和一张大王,因此该牌型要求两张同时在场。
校验逻辑
cards.length !== 2→nullpoints = Set([getPoint(cards[0]), getPoint(cards[1])])!points.has(SMALL_JOKER_POINT) || !points.has(BIG_JOKER_POINT)→null- 否则 →
makeCombination(ROCKET, cards, BIG_JOKER_POINT)(mainRank = 17,供比较器使用:rocket > bomb)
detectFourWithTwo(FOUR_WITH_TWO)
import { detectFourWithTwo } from './detectors/FourWithTwoDetector.js'
function detectFourWithTwo(cards: Card[]): DoudizhuCombination | nullFOUR_WITH_TWO(四带二):四张同点 + 附件牌。
Phase 5 规则
- 6 张牌:四张同点 + 2 张附件牌。2 张附件按"两张单牌"处理;它们可以来自同一点数(例如
333355,其中 55 虽是对子仍按两张单牌计)。附件不得包含count >= 4的点数(否则与 BOMB 变体产生歧义)。 - 8 张牌:四张同点 + 2 个互异对子(每个附件点数恰好
count为 2,来自互异点数)。
四张点数即 mainRank。四张 + 2 张王(小王 + 大王)在此被拒绝,因为这会与 ROCKET 优先级冲突——而且 ROCKET 在优先级链中更早被检测,此守卫仅为防御性。
校验逻辑
cards.length !== 6 && cards.length !== 8→null- 找到唯一的四张点数
quad = counts.find(rc => rc.count >= 4);不存在 →null - 不允许两个点数同时
count >= 4(会产生歧义)→null - 收集剩余牌(四张点数的余量 + 其他点数的全部)作为
attachments; - 若
cards.length === 6:要求attachments.length === 2; - 若
cards.length === 8:要求attachments.length === 4,且otherCounts = rankCounts(attachments)长度为 2 且每点count === 2; - 否则 →
makeCombination(FOUR_WITH_TWO, cards, quad.point, { attachments })
示例
333355 → 四张(3) + 2 单张(5,5) ✓ (长度 6)
33335566 → 四张(3) + 2 对子(55,66) ✓ (长度 8)
333344 → 四张(3) + 对子(44) ✓ (长度 6,按 2 张单牌处理)
3333A → 太短 ✗ (长度 5)detectTripleWithSingle(TRIPLE_WITH_SINGLE)
import { detectTripleWithSingle } from './detectors/TripleWithSingleDetector.js'
function detectTripleWithSingle(cards: Card[]): DoudizhuCombination | nullTRIPLE_WITH_SINGLE(三带一):一个三张(三张同点)+ 一张单张附件。共 4 张牌。附件必须来自与三张不同的点数(这样四张同点就不会被误判为本牌型)。
校验逻辑
cards.length !== 4→nullrankCounts(cards).length !== 2→null- 找
triple = counts.find(rc => rc.count === 3)与single = counts.find(rc => rc.count === 1);任一不存在 →null - 否则 →
makeCombination(TRIPLE_WITH_SINGLE, cards, triple.point, { attachments: single.cards })
示例
3335 → 三张(3) + 单张(5)detectTripleWithPair(TRIPLE_WITH_PAIR)
import { detectTripleWithPair } from './detectors/TripleWithPairDetector.js'
function detectTripleWithPair(cards: Card[]): DoudizhuCombination | nullTRIPLE_WITH_PAIR(三带二):一个三张(三张同点)+ 一个对子(两张同点)。共 5 张牌。三张与对子必须是不同点数。
校验逻辑
cards.length !== 5→nullrankCounts(cards).length !== 2→null- 找
triple = counts.find(rc => rc.count === 3)与pair = counts.find(rc => rc.count === 2);任一不存在 →null - 否则 →
makeCombination(TRIPLE_WITH_PAIR, cards, triple.point, { attachments: pair.cards })
示例
33355 → 三张(3) + 对子(5)DoudizhuCombinationComparator
class DoudizhuCombinationComparator {
compare(a: DoudizhuCombination, b: DoudizhuCombination): DoudizhuCompareResult
}实现斗地主的"可压过"规则。
比较规则
- ROCKET 压一切。Rocket vs 任意 →
GREATER,除非双方均为 rocket →EQUAL。 - BOMB 压任意非炸弹、非 rocket 牌型。Bomb vs bomb → 比较主点数。Bomb vs rocket →
LESS。 - 非炸弹 vs 非炸弹:仅当牌型与长度均相同时可比较(链式牌型还要求
chainLength相同)。然后比较主点数。若牌型或长度不同 →INCOMPARABLE。
compare()
compare(a: DoudizhuCombination, b: DoudizhuCombination): DoudizhuCompareResult参数
a: DoudizhuCombination— 牌型 ab: DoudizhuCombination— 牌型 b
返回值
DoudizhuCompareResult—LESS/EQUAL/GREATER/INCOMPARABLE
行为
- 双方 rocket →
EQUAL;仅 a rocket →GREATER;仅 b rocket →LESS; - 双方 bomb →
cmpRank(a.mainRank, b.mainRank);仅 a bomb →GREATER;仅 b bomb →LESS; - 非炸弹 vs 非炸弹:
a.type !== b.type→INCOMPARABLEa.length !== b.length→INCOMPARABLEa.chainLength !== b.chainLength→INCOMPARABLE(防御性写法——对当前检测器而言长度相等已隐含此条件,但显式校验可避免未来检测器变更时静默失效)- 否则 →
cmpRank(a.mainRank, b.mainRank)
beats()
function beats(
a: DoudizhuCombination,
b: DoudizhuCombination,
comparator: DoudizhuCombinationComparator = new DoudizhuCombinationComparator()
): boolean判断牌型 a 是否严格大于 b(即可压过 b)。EQUAL / INCOMPARABLE 均视为不能压过。
参数
a: DoudizhuCombination— 牌型 ab: DoudizhuCombination— 牌型 bcomparator: DoudizhuCombinationComparator— 比较器,默认新建一个
返回值
boolean—a > b时为true
示例
import {
detectDoudizhuCombination,
beats,
DoudizhuCombinationType
} from 'decklet/doudizhu'
import { createDoudizhuCards } from 'decklet/doudizhu'
const all = createDoudizhuCards()
const c3 = all.find((c) => c.id === 'ddz-♠-3')!
const c4 = all.find((c) => c.id === 'ddz-♠-4')!
const single4 = detectDoudizhuCombination([c4])
const single3 = detectDoudizhuCombination([c3])
console.log(beats(single4, single3)) // true(4 > 3)DoudizhuCompareResult
enum DoudizhuCompareResult {
LESS = -1,
EQUAL = 0,
GREATER = 1,
INCOMPARABLE = -2
}比较两个斗地主牌型的结果。
| 枚举值 | 说明 |
|---|---|
LESS | 严格序关系:b 可压过 a |
EQUAL | 牌型相同 + 主点数相同(链式牌型还要求链长相同)。注意:相等的牌型通常不会相互出牌(不能用 PAIR 33 压另一个 PAIR 33),因此 PlayCards 规则将 EQUAL 视为"不能压过" |
GREATER | 严格序关系:a 可压过 b |
INCOMPARABLE | a 与 b 是不同形态、无法比较(例如 PAIR vs TRIPLE,或长度 5 的 STRAIGHT vs 长度 6 的 STRAIGHT)。唯一例外是炸弹系列:BOMB 可压任意非炸弹牌型,ROCKET 可压一切(包括炸弹) |
DoudizhuRankResolver
import { DoudizhuRankResolver } from './DoudizhuRankResolver.js'
const DoudizhuRankResolver: RankResolver斗地主点数解析器。返回卡牌上存储的数值点数(花色牌 3..15;大小王 16/17)。这与扑克的 StandardPokerRankResolver(其中 A=14、2=2)不同,因此斗地主插件使用自己的解析器,不复用扑克那一个。
该解析器实现了核心 RankResolver 接口,使得通用的 CardQuery 工具(groupByRank、countByRank、sortByRank)可在斗地主语义下复用。
resolveRank()
resolveRank(card: Card): number参数
card: Card— 卡牌
返回值
number— 卡牌的数值点数(缺失时为 0)。内部调用getPoint(card as DoudizhuCard | undefined)。
示例
import { DoudizhuRankResolver, createDoudizhuCards } from 'decklet/doudizhu'
const all = createDoudizhuCards()
const card3 = all.find((c) => c.id === 'ddz-♠-3')!
console.log(DoudizhuRankResolver.resolveRank(card3)) // 3
const bigJoker = all.find((c) => c.id === 'ddz-BJ')!
console.log(DoudizhuRankResolver.resolveRank(bigJoker)) // 17注意事项
DoudizhuRankResolver 由 DoudizhuRankResolver.ts 直接导出,供检测器内部使用;它未在 src/index.ts 中重新导出,调用方需从文件路径直接导入。
共享工具函数(helpers)
来源:combinations/detectors/helpers.ts。所有函数均为纯的、无状态的;只读取卡牌并返回描述符。检测器组合使用这些函数来判断一组卡牌是否匹配其牌型形态。
RankCount
type RankCount = { point: number; count: number; cards: Card[] }按点数聚合后的卡牌描述。
| 字段 | 类型 | 说明 |
|---|---|---|
point | number | 点数(3..17) |
count | number | 该点数卡牌数量 |
cards | Card[] | 该点数的全部卡牌 |
sortedAsc()
function sortedAsc(cards: Card[]): Card[]按斗地主点数升序排序卡牌。内部调用 sortByRank(cards, DoudizhuRankResolver)。
rankCounts()
function rankCounts(cards: Card[]): RankCount[]按点数分组,返回按点数升序排列的逐点描述符。
返回值
RankCount[]— 升序排列的RankCount数组
findConsecutiveRun()
function findConsecutiveRun(
counts: RankCount[],
minPerPoint: number,
minLength: number
): number[] | null查找最长的一段连续可链接点数(3..A)序列,要求序列中每个点数的数量 >= minPerPoint。返回该序列的点数列表(升序);若不存在长度 >= minLength 的序列则返回 null。
参数
counts: RankCount[]—rankCounts的输出minPerPoint: number— 序列中每点最少卡牌数minLength: number— 序列最少点数数量
返回值
number[] | null— 最长连续段点数列表;不存在时为null
用途
用于 STRAIGHT(minPerPoint=1, minLength=5)、CONSECUTIVE_PAIRS(minPerPoint=2, minLength=3)和 AIRPLANE*(minPerPoint=3, minLength=2)。
leftoverAfter()
function leftoverAfter(
counts: RankCount[],
chainPoints: number[],
usedPerPoint: number
): RankCount[]将按点数聚合的描述符拆分为"主体链点数"与"剩余点数"。返回剩余点数描述符(point + count + cards),不含主体链点数。若某点数被链部分使用(count > min),剩余的 (count - min) 张保留在剩余描述符中。
totalCards()
function totalCards(counts: RankCount[]): number所有点数描述符的卡牌总数。
sumCount()
function sumCount(counts: RankCount[], points: number[]): number给定点数列表对应的卡牌数量之和。
pointOf()
function pointOf(card: Card): number读取一张卡牌的点数(用于直接操作 Card[] 的检测器)。内部调用 getPoint(card as DoudizhuCard | undefined)。
canChain(重新导出)
export { canChain }helpers.ts 重新导出 canChain(来自 cards/DoudizhuCard.ts),便于检测器直接从 helpers 导入。详见 cards.md 的 canChain。
完整对照表
| 牌型 | Detector | 长度约束 | mainRank | chainLength | attachments |
|---|---|---|---|---|---|
| ROCKET | detectRocket | 2 | BIG_JOKER_POINT(17) | - | - |
| BOMB | detectBomb | 4 | 四张点数 | - | - |
| AIRPLANE_WITH_PAIR | detectAirplaneWithPair | 5*N, N≥2 | 主体最小点 | N | N 个对子 |
| AIRPLANE_WITH_SINGLE | detectAirplaneWithSingle | 4*N, N≥2 | 主体最小点 | N | N 个单张 |
| AIRPLANE | detectAirplane | 3*N, N≥2 | 主体最小点 | N | - |
| FOUR_WITH_TWO | detectFourWithTwo | 6 或 8 | 四张点数 | - | 2 单张 或 2 对子 |
| STRAIGHT | detectStraight | ≥5 | 最小点 | 长度 | - |
| CONSECUTIVE_PAIRS | detectConsecutivePairs | 2*N, N≥3 | 最小点 | N | - |
| TRIPLE_WITH_PAIR | detectTripleWithPair | 5 | 三张点数 | - | 对子 |
| TRIPLE_WITH_SINGLE | detectTripleWithSingle | 4 | 三张点数 | - | 单张 |
| TRIPLE | detectTriple | 3 | 点数 | - | - |
| PAIR | detectPair | 2 | 点数 | - | - |
| SINGLE | detectSingle | 1 | 点数 | - | - |
注意事项
- 检测器优先级顺序(
DETECTORS列表)很重要:例如333344长度 6 会先经detectBomb(4 张不满足,因为 counts.length=2)→detectAirplane*(不满足)→detectFourWithTwo(命中,按 2 张单牌处理)。 AIRPLANE_WITH_SINGLE与AIRPLANE_WITH_PAIR的findRunOfLength仅匹配"恰好"长度为N的连续段——更长的连续段不匹配,以避免对飞机主体的歧义拆分。待确认:若一手牌同时包含长度 2 和长度 3 的连续三张段,且非主体牌数量恰好满足 N=2,检测器会优先匹配长度 2 的段;这可能不是玩家意图的最长匹配。DoudizhuCombinationComparator中链式牌型比较要求chainLength相同——这是防御性写法,对当前检测器而言长度相等已隐含此条件。- 所有 Detector 与 helper 函数均为文件级导出(
detectors/*.ts);只有DETECTORS、detectDoudizhuCombination、isBombFamily、DoudizhuCombinationDetector在src/index.ts中重新导出。如需直接使用单个 Detector(如detectSingle),需从文件路径导入。