Skip to content

Combinations ​

斗地主牌型系统:13 种牌型枚举、牌型对象、检测器总入口、13 个独立 Detector、比较器、点数解析器与共享工具函数。

概述 ​

斗地主牌型系统由以下模块构成:

模块文件说明
牌型枚举DoudizhuCombinationType.ts13 种牌型 + UNKNOWN 兜底
牌型对象DoudizhuCombination.tsDoudizhuCombination 接口与 makeCombination / unknownCombination 工厂
检测器总入口DoudizhuCombinationDetector.tsDETECTORS 列表、DoudizhuCombinationDetector 类、detectDoudizhuCombination / isBombFamily
13 个 Detectordetectors/*.ts每个牌型一个纯函数 Card[] -> DoudizhuCombination | null
比较器DoudizhuCombinationComparator.ts"可压过"规则与 DoudizhuCompareResult 枚举
点数解析器DoudizhuRankResolver.ts实现 RankResolver 接口,供通用 CardQuery 复用
共享工具detectors/helpers.tsrankCounts / findConsecutiveRun / leftoverAfter 等

DoudizhuCombinationType ​

ts
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() ​

ts
function combinationTypeName(t: DoudizhuCombinationType): string

牌型类型的人类可读名称。当前实现为恒等函数(直接返回入参)。


DoudizhuCombination ​

ts
interface DoudizhuCombination {
  type: DoudizhuCombinationType
  cards: Card[]
  mainRank: number
  length: number
  chainLength?: number
  attachments?: Card[]
  metadata?: Record<string, unknown>
}

在一组卡牌上识别出的斗地主牌型。

字段类型说明
typeDoudizhuCombinationType命中的斗地主牌型形态
cardsCard[]产生该牌型的完整卡牌集合
mainRanknumber用于比较的主点数。对于链式牌型(STRAIGHT/CONSECUTIVE_PAIRS/AIRPLANE*)为链中的最小点数;对于 BOMB/FOUR_WITH_TWO 为四张的点数;对于 ROCKET 为 BIG_JOKER_POINT(17)
lengthnumber该牌型中的卡牌总数
chainLength?number | undefined链式牌型:链单元数量(顺子 = 单张数量;连对 = 对子数量;飞机变体 = 三张数量)。非链式牌型为 undefined
attachments?Card[]附挂于三张/四张/飞机的附件牌(单张/对子)。纯牌型为 undefined
metadata?Record<string, unknown>自由结构,供检测器记录额外细节(例如附件拆分明细)

makeCombination() ​

ts
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() ​

ts
function unknownCombination(cards: Card[]): DoudizhuCombination

构造无法识别的 UNKNOWN 牌型(mainRank=0)。用于检测器全部不匹配时的兜底。

参数 ​

  • cards: Card[] — 未识别的卡牌集合

返回值 ​

  • DoudizhuCombination — { type: UNKNOWN, cards, mainRank: 0, length: cards.length }

DoudizhuCombinationDetector(检测器总入口) ​

来源:combinations/DoudizhuCombinationDetector.ts。

DETECTORS ​

ts
const DETECTORS: ReadonlyArray<
  (cards: Card[]) => DoudizhuCombination | null
>

斗地主牌型检测器(Detector)的有序列表。顺序很重要:当一组牌可匹配多种形态时,首个匹配胜出。此顺序对应 Phase 5 规范中的检测优先级:

序号检测器牌型
1detectRocketROCKET
2detectBombBOMB
3detectAirplaneWithPairAIRPLANE_WITH_PAIR
4detectAirplaneWithSingleAIRPLANE_WITH_SINGLE
5detectAirplaneAIRPLANE
6detectFourWithTwoFOUR_WITH_TWO
7detectStraightSTRAIGHT
8detectConsecutivePairsCONSECUTIVE_PAIRS
9detectTripleWithPairTRIPLE_WITH_PAIR
10detectTripleWithSingleTRIPLE_WITH_SINGLE
11detectTripleTRIPLE
12detectPairPAIR
13detectSingleSINGLE

该列表对外导出,便于插件 / 测试检视或重排。每个条目都是纯函数 Card[] -> DoudizhuCombination | null。

DoudizhuCombinationDetector 类 ​

ts
class DoudizhuCombinationDetector {
  constructor(
    detectors?: ReadonlyArray<(cards: Card[]) => DoudizhuCombination | null>
  )
  detect(cards: readonly Card[]): DoudizhuCombination
}

按优先级顺序对输入卡牌集合运行检测器列表,返回首个匹配结果。若无任何检测器匹配,返回 UNKNOWN 牌型。

该检测器有意设计为纯的、无状态的类(不耦合引擎)。其 API 与扑克的 CombinationDetector 对齐,下游代码(规则、测试)可对称地使用它们。

构造函数 ​

ts
constructor(
  detectors?: ReadonlyArray<(cards: Card[]) => DoudizhuCombination | null>
)
参数 ​
  • detectors? — 自定义检测器列表。省略时使用默认 DETECTORS 列表。

detect() ​

ts
detect(cards: readonly Card[]): DoudizhuCombination
参数 ​
  • cards: readonly Card[] — 待检测的卡牌
返回值 ​
  • DoudizhuCombination — 识别到的牌型;无匹配时为 UNKNOWN
行为 ​
  • cards.length === 0 → 直接返回 unknownCombination([]);
  • 否则按顺序运行 this.detectors 中每个检测器,传入 [...cards](复制以避免检测器污染原数组),首个返回非 null 的结果胜出;
  • 全部不匹配 → 返回 unknownCombination([...cards])。

detectDoudizhuCombination() ​

ts
function detectDoudizhuCombination(cards: readonly Card[]): DoudizhuCombination

便捷纯函数:使用默认检测器顺序从一组卡牌中检测牌型。无状态——可在任何位置安全调用。

示例 ​

ts
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)                                // 2

isBombFamily() ​

ts
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) ​

ts
import { detectSingle } from './detectors/SingleDetector.js'
function detectSingle(cards: Card[]): DoudizhuCombination | null

SINGLE(单张):恰好一张牌。任意一张牌(花色牌或王)都是合法的单张。

校验逻辑 ​

  • cards.length !== 1 → null
  • 否则 → makeCombination(SINGLE, cards, pointOf(cards[0]))

示例 ​

ts
// 任意一张牌 → SINGLE,mainRank 为该牌点数
detectSingle([card3]) // { type: SINGLE, mainRank: 3, length: 1 }

detectPair(PAIR) ​

ts
import { detectPair } from './detectors/PairDetector.js'
function detectPair(cards: Card[]): DoudizhuCombination | null

PAIR(对子):两张同点数的牌。由于每张王都是唯一的(一副牌中只有一张小王和一张大王),两张王不能组成对子——只有两张同点数的花色牌才算合法对子。

校验逻辑 ​

  • cards.length !== 2 → null
  • pointOf(cards[0]) !== pointOf(cards[1]) → null
  • 否则 → makeCombination(PAIR, cards, pointOf(cards[0]))

示例 ​

ts
detectPair([card3s, card3h]) // PAIR,mainRank=3
detectPair([smallJoker, bigJoker]) // null(两张王不是 PAIR,会由 detectRocket 命中)

detectTriple(TRIPLE) ​

ts
import { detectTriple } from './detectors/TripleDetector.js'
function detectTriple(cards: Card[]): DoudizhuCombination | null

TRIPLE(三张):恰好三张同点数的牌(例如 333)。由于每张王都只有一张,王不能组成三张。

校验逻辑 ​

  • cards.length !== 3 → null
  • rankCounts(cards).length !== 1 → null(必须只有一种点数)
  • counts[0].count !== 3 → null
  • 否则 → makeCombination(TRIPLE, cards, counts[0].point)

detectStraight(STRAIGHT) ​

ts
import { detectStraight } from './detectors/StraightDetector.js'
function detectStraight(cards: Card[]): DoudizhuCombination | null

STRAIGHT(顺子):5+ 张连续可链接的单牌。可链接点数为 3..A(即点数 3..14)。2(15)和王(16/17)不能参与。

校验逻辑 ​

  • cards.length < 5 → null
  • rankCounts(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) ​

ts
import { detectConsecutivePairs } from './detectors/ConsecutivePairsDetector.js'
function detectConsecutivePairs(cards: Card[]): DoudizhuCombination | null

CONSECUTIVE_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) ​

ts
import { detectAirplane } from './detectors/AirplaneDetector.js'
function detectAirplane(cards: Card[]): DoudizhuCombination | null

AIRPLANE(飞机):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) ​

ts
import { detectAirplaneWithSingle } from './detectors/AirplaneWithSingleDetector.js'
function detectAirplaneWithSingle(cards: Card[]): DoudizhuCombination | null

AIRPLANE_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 → null
  • N = 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) ​

ts
import { detectAirplaneWithPair } from './detectors/AirplaneWithPairDetector.js'
function detectAirplaneWithPair(cards: Card[]): DoudizhuCombination | null

AIRPLANE_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 → null
  • N = 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) ​

ts
import { detectBomb } from './detectors/BombDetector.js'
function detectBomb(cards: Card[]): DoudizhuCombination | null

BOMB(炸弹):恰好四张同点数的牌(例如 3333、AAAA、2222)。压任意非 rocket 牌型。

校验逻辑 ​

  • cards.length !== 4 → null
  • rankCounts(cards).length !== 1 → null
  • counts[0].count !== 4 → null
  • 否则 → makeCombination(BOMB, cards, counts[0].point)

detectRocket(ROCKET) ​

ts
import { detectRocket } from './detectors/RocketDetector.js'
function detectRocket(cards: Card[]): DoudizhuCombination | null

ROCKET(火箭 / 王炸):小王 + 大王。压一切(包括炸弹)。一副牌中恰好只有一张小王和一张大王,因此该牌型要求两张同时在场。

校验逻辑 ​

  • cards.length !== 2 → null
  • points = 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) ​

ts
import { detectFourWithTwo } from './detectors/FourWithTwoDetector.js'
function detectFourWithTwo(cards: Card[]): DoudizhuCombination | null

FOUR_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) ​

ts
import { detectTripleWithSingle } from './detectors/TripleWithSingleDetector.js'
function detectTripleWithSingle(cards: Card[]): DoudizhuCombination | null

TRIPLE_WITH_SINGLE(三带一):一个三张(三张同点)+ 一张单张附件。共 4 张牌。附件必须来自与三张不同的点数(这样四张同点就不会被误判为本牌型)。

校验逻辑 ​

  • cards.length !== 4 → null
  • rankCounts(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) ​

ts
import { detectTripleWithPair } from './detectors/TripleWithPairDetector.js'
function detectTripleWithPair(cards: Card[]): DoudizhuCombination | null

TRIPLE_WITH_PAIR(三带二):一个三张(三张同点)+ 一个对子(两张同点)。共 5 张牌。三张与对子必须是不同点数。

校验逻辑 ​

  • cards.length !== 5 → null
  • rankCounts(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 ​

ts
class DoudizhuCombinationComparator {
  compare(a: DoudizhuCombination, b: DoudizhuCombination): DoudizhuCompareResult
}

实现斗地主的"可压过"规则。

比较规则 ​

  1. ROCKET 压一切。Rocket vs 任意 → GREATER,除非双方均为 rocket → EQUAL。
  2. BOMB 压任意非炸弹、非 rocket 牌型。Bomb vs bomb → 比较主点数。Bomb vs rocket → LESS。
  3. 非炸弹 vs 非炸弹:仅当牌型与长度均相同时可比较(链式牌型还要求 chainLength 相同)。然后比较主点数。若牌型或长度不同 → INCOMPARABLE。

compare() ​

ts
compare(a: DoudizhuCombination, b: DoudizhuCombination): DoudizhuCompareResult

参数 ​

  • a: DoudizhuCombination — 牌型 a
  • b: DoudizhuCombination — 牌型 b

返回值 ​

  • DoudizhuCompareResult — LESS / EQUAL / GREATER / INCOMPARABLE

行为 ​

  1. 双方 rocket → EQUAL;仅 a rocket → GREATER;仅 b rocket → LESS;
  2. 双方 bomb → cmpRank(a.mainRank, b.mainRank);仅 a bomb → GREATER;仅 b bomb → LESS;
  3. 非炸弹 vs 非炸弹:
    • a.type !== b.type → INCOMPARABLE
    • a.length !== b.length → INCOMPARABLE
    • a.chainLength !== b.chainLength → INCOMPARABLE(防御性写法——对当前检测器而言长度相等已隐含此条件,但显式校验可避免未来检测器变更时静默失效)
    • 否则 → cmpRank(a.mainRank, b.mainRank)

beats() ​

ts
function beats(
  a: DoudizhuCombination,
  b: DoudizhuCombination,
  comparator: DoudizhuCombinationComparator = new DoudizhuCombinationComparator()
): boolean

判断牌型 a 是否严格大于 b(即可压过 b)。EQUAL / INCOMPARABLE 均视为不能压过。

参数 ​

  • a: DoudizhuCombination — 牌型 a
  • b: DoudizhuCombination — 牌型 b
  • comparator: DoudizhuCombinationComparator — 比较器,默认新建一个

返回值 ​

  • boolean — a > b 时为 true

示例 ​

ts
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 ​

ts
enum DoudizhuCompareResult {
  LESS = -1,
  EQUAL = 0,
  GREATER = 1,
  INCOMPARABLE = -2
}

比较两个斗地主牌型的结果。

枚举值说明
LESS严格序关系:b 可压过 a
EQUAL牌型相同 + 主点数相同(链式牌型还要求链长相同)。注意:相等的牌型通常不会相互出牌(不能用 PAIR 33 压另一个 PAIR 33),因此 PlayCards 规则将 EQUAL 视为"不能压过"
GREATER严格序关系:a 可压过 b
INCOMPARABLEa 与 b 是不同形态、无法比较(例如 PAIR vs TRIPLE,或长度 5 的 STRAIGHT vs 长度 6 的 STRAIGHT)。唯一例外是炸弹系列:BOMB 可压任意非炸弹牌型,ROCKET 可压一切(包括炸弹)

DoudizhuRankResolver ​

ts
import { DoudizhuRankResolver } from './DoudizhuRankResolver.js'
const DoudizhuRankResolver: RankResolver

斗地主点数解析器。返回卡牌上存储的数值点数(花色牌 3..15;大小王 16/17)。这与扑克的 StandardPokerRankResolver(其中 A=14、2=2)不同,因此斗地主插件使用自己的解析器,不复用扑克那一个。

该解析器实现了核心 RankResolver 接口,使得通用的 CardQuery 工具(groupByRank、countByRank、sortByRank)可在斗地主语义下复用。

resolveRank() ​

ts
resolveRank(card: Card): number

参数 ​

  • card: Card — 卡牌

返回值 ​

  • number — 卡牌的数值点数(缺失时为 0)。内部调用 getPoint(card as DoudizhuCard | undefined)。

示例 ​

ts
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 ​

ts
type RankCount = { point: number; count: number; cards: Card[] }

按点数聚合后的卡牌描述。

字段类型说明
pointnumber点数(3..17)
countnumber该点数卡牌数量
cardsCard[]该点数的全部卡牌

sortedAsc() ​

ts
function sortedAsc(cards: Card[]): Card[]

按斗地主点数升序排序卡牌。内部调用 sortByRank(cards, DoudizhuRankResolver)。

rankCounts() ​

ts
function rankCounts(cards: Card[]): RankCount[]

按点数分组,返回按点数升序排列的逐点描述符。

返回值 ​

  • RankCount[] — 升序排列的 RankCount 数组

findConsecutiveRun() ​

ts
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() ​

ts
function leftoverAfter(
  counts: RankCount[],
  chainPoints: number[],
  usedPerPoint: number
): RankCount[]

将按点数聚合的描述符拆分为"主体链点数"与"剩余点数"。返回剩余点数描述符(point + count + cards),不含主体链点数。若某点数被链部分使用(count > min),剩余的 (count - min) 张保留在剩余描述符中。

totalCards() ​

ts
function totalCards(counts: RankCount[]): number

所有点数描述符的卡牌总数。

sumCount() ​

ts
function sumCount(counts: RankCount[], points: number[]): number

给定点数列表对应的卡牌数量之和。

pointOf() ​

ts
function pointOf(card: Card): number

读取一张卡牌的点数(用于直接操作 Card[] 的检测器)。内部调用 getPoint(card as DoudizhuCard | undefined)。

canChain(重新导出) ​

ts
export { canChain }

helpers.ts 重新导出 canChain(来自 cards/DoudizhuCard.ts),便于检测器直接从 helpers 导入。详见 cards.md 的 canChain。


完整对照表 ​

牌型Detector长度约束mainRankchainLengthattachments
ROCKETdetectRocket2BIG_JOKER_POINT(17)--
BOMBdetectBomb4四张点数--
AIRPLANE_WITH_PAIRdetectAirplaneWithPair5*N, N≥2主体最小点NN 个对子
AIRPLANE_WITH_SINGLEdetectAirplaneWithSingle4*N, N≥2主体最小点NN 个单张
AIRPLANEdetectAirplane3*N, N≥2主体最小点N-
FOUR_WITH_TWOdetectFourWithTwo6 或 8四张点数-2 单张 或 2 对子
STRAIGHTdetectStraight≥5最小点长度-
CONSECUTIVE_PAIRSdetectConsecutivePairs2*N, N≥3最小点N-
TRIPLE_WITH_PAIRdetectTripleWithPair5三张点数-对子
TRIPLE_WITH_SINGLEdetectTripleWithSingle4三张点数-单张
TRIPLEdetectTriple3点数--
PAIRdetectPair2点数--
SINGLEdetectSingle1点数--

注意事项 ​

  • 检测器优先级顺序(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),需从文件路径导入。