Cards
斗地主卡牌、牌堆组成与花色定义。
概述
斗地主使用标准 54 张牌:4 种花色 × 13 个点数(共 52 张)+ 1 张小王 + 1 张大王。点数采用斗地主专用排序(3 最小,大王最大),与扑克的 StandardPokerRankResolver 不同。
| 模块 | 说明 |
|---|---|
DoudizhuCardData | 卡牌 payload 接口(point + isJoker) |
DoudizhuCard | 卡牌类型(Card<DoudizhuCardData>) |
| 点数常量 | SMALL_JOKER_POINT / BIG_JOKER_POINT / MIN_CHAIN_POINT / MAX_CHAIN_POINT |
| 卡牌函数 | getPoint / canChain / asDoudizhuCard / doudizhuCardFace / rankLabel |
| 牌堆常量 | DOUDIZHU_DECK_SIZE / DOUDIZHU_HAND_SIZE / DOUDIZHU_BOTTOM_SIZE / DOUDIZHU_PLAYER_COUNT / DOUDIZHU_SUITS / DOUDIZHU_RANKS / DoudizhuSuit |
| 牌堆工厂 | createDoudizhuCards / createShuffledDoudizhuDeck |
点数排序
斗地主卡牌的数值点数(point)遵循斗地主的牌力排序(数值越小越弱):
| 点数 | 标签 | 说明 |
|---|---|---|
| 3..10 | 3..10 | 普通花色牌 |
| 11 | J | 普通花色牌 |
| 12 | Q | 普通花色牌 |
| 13 | K | 普通花色牌 |
| 14 | A | 普通花色牌,可参与连子 |
| 15 | 2 | 不能进入顺子 / 连对 / 飞机 |
| 16 | SJ | 小王,无花色 |
| 17 | BJ | 大王;压小王,无花色 |
isJoker 区分两张王与花色牌。王无花色,不能参与任何连子型牌型;仅大小王组合的 ROCKET(火箭)牌型会使用它们。
DoudizhuCardData
interface DoudizhuCardData {
/** 用于比较大小与连子检测的数值点数。 */
point: number
/** 大小王为 true;花色牌为 false。 */
isJoker: boolean
}斗地主卡牌的 payload。
| 字段 | 类型 | 说明 |
|---|---|---|
point | number | 用于比较大小与连子检测的数值点数 |
isJoker | boolean | 大小王为 true;花色牌为 false |
DoudizhuCard
type DoudizhuCard = Card<DoudizhuCardData>斗地主卡牌类型,等价于 Card<DoudizhuCardData>。继承核心 Card 的 id / type / suit? / rank? / value / tags / data 字段,其中 data 即 DoudizhuCardData。
点数常量
SMALL_JOKER_POINT
const SMALL_JOKER_POINT = 16小王点数值(16)。
BIG_JOKER_POINT
const BIG_JOKER_POINT = 17大王点数值(17)。
MIN_CHAIN_POINT
const MIN_CHAIN_POINT = 3可参与连子的最低点数(3)。2(15)和王不可连子。
MAX_CHAIN_POINT
const MAX_CHAIN_POINT = 14可参与连子的最高点数(A = 14)。
卡牌函数
getPoint()
function getPoint(card: DoudizhuCard | undefined): number读取一张斗地主卡牌的数值点数(缺失时默认为 0)。
参数
card: DoudizhuCard | undefined— 卡牌(可为 undefined)
返回值
number—card?.data?.point ?? 0
canChain()
function canChain(point: number): boolean判断该点数是否可参与连子(顺子 / 连对 / 飞机)。
参数
point: number— 点数
返回值
boolean—point >= MIN_CHAIN_POINT && point <= MAX_CHAIN_POINT(即3 <= point <= 14)
示例
import { canChain } from 'decklet/doudizhu'
console.log(canChain(3)) // true
console.log(canChain(14)) // true(A)
console.log(canChain(15)) // false(2 不能连子)
console.log(canChain(16)) // false(小王)asDoudizhuCard()
function asDoudizhuCard(
card: { type: string; data: unknown } | undefined
): DoudizhuCard | undefined通过检查 data payload 将通用 Card 收窄为 DoudizhuCard。若卡牌未携带斗地主数据则返回 undefined。供 Rule 与 Handler 安全地进行 Card -> DoudizhuCard 类型收窄使用。
参数
card: { type: string; data: unknown } | undefined— 通用卡牌对象
返回值
DoudizhuCard | undefined— 若card.data是对象且同时包含point与isJoker字段,则返回card as DoudizhuCard;否则undefined
行为
card为null/undefined→undefinedcard.data为null或非对象 →undefinedcard.data不含point或isJoker字段 →undefined- 否则 → 将
card视为DoudizhuCard返回
示例
import { asDoudizhuCard, createDoudizhuCards } from 'decklet/doudizhu'
const all = createDoudizhuCards()
const card = all[0]!
const ddz = asDoudizhuCard(card)
if (ddz) {
console.log(ddz.data.point, ddz.data.isJoker)
}doudizhuCardFace()
function doudizhuCardFace(card: DoudizhuCard): string人类可读的卡面标签,如 "♥A"、"♠2"、"SJ"、"BJ"。
参数
card: DoudizhuCard— 斗地主卡牌
返回值
string— 卡面标签:- 王:
card.data.point === BIG_JOKER_POINT→'BJ',否则'SJ' - 花色牌:
${suit}${rankLabel(point)}(suit 缺失时为'?')
- 王:
示例
import { doudizhuCardFace, createDoudizhuCards } from 'decklet/doudizhu'
const all = createDoudizhuCards()
console.log(doudizhuCardFace(all.find((c) => c.id === 'ddz-♥-A')!)) // '♥A'
console.log(doudizhuCardFace(all.find((c) => c.id === 'ddz-BJ')!)) // 'BJ'
console.log(doudizhuCardFace(all.find((c) => c.id === 'ddz-SJ')!)) // 'SJ'rankLabel()
function rankLabel(point: number): string将数值点数(3..15)转换为显示标签。
参数
point: number— 数值点数
返回值
string— 显示标签:11→'J'12→'Q'13→'K'14→'A'15→'2'- 其它 →
String(point)(如3→'3',10→'10')
牌堆常量
DOUDIZHU_DECK_SIZE
const DOUDIZHU_DECK_SIZE = 54标准斗地主牌堆总张数(52 花色牌 + 2 王)。
DOUDIZHU_HAND_SIZE
const DOUDIZHU_HAND_SIZE = 17发给 3 位玩家每人手牌的张数(共 17 * 3 = 51 张,剩 3 张作底牌)。
DOUDIZHU_BOTTOM_SIZE
const DOUDIZHU_BOTTOM_SIZE = 3作为底牌保留的张数,后续亮给地主(地主最终持 17 + 3 = 20 张)。
DOUDIZHU_PLAYER_COUNT
const DOUDIZHU_PLAYER_COUNT = 3标准斗地主的玩家数量。
DOUDIZHU_SUITS
const DOUDIZHU_SUITS = ['♠', '♥', '♣', '♦'] as const斗地主使用的 4 种花色(黑桃 / 红桃 / 梅花 / 方块)。王不属于任何花色。
DoudizhuSuit
type DoudizhuSuit = (typeof DOUDIZHU_SUITS)[number]斗地主花色字面量类型,由 DOUDIZHU_SUITS 推导,即 '♠' | '♥' | '♣' | '♦'。
DOUDIZHU_RANKS
const DOUDIZHU_RANKS = [
'3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A', '2'
] as const按点数顺序(3..2)排列的稳定点数标签。注意 '2' 排在最后(点数 15,最大),'A' 排在 '2' 前(点数 14)。
牌堆工厂
createDoudizhuCards()
function createDoudizhuCards(): DoudizhuCard[]按稳定的规范顺序构建 54 张斗地主牌堆(未洗牌)。
参数
无。
返回值
DoudizhuCard[]— 54 张卡牌,顺序为:先按DOUDIZHU_RANKS顺序遍历 13 个点数,每个点数按DOUDIZHU_SUITS顺序遍历 4 种花色(共 52 张花色牌),最后追加小王(ddz-SJ)和大王(ddz-BJ)。
卡牌 id 规范
每张牌的 id 采用规范格式,保证同一副牌内 id 稳定可复现(便于回放与测试断言):
| 牌 | id | type | data |
|---|---|---|---|
| 黑桃 3 | 'ddz-♠-3' | 'standard' | { point: 3, isJoker: false } |
| 红桃 A | 'ddz-♥-A' | 'standard' | { point: 14, isJoker: false } |
| 方块 2 | 'ddz-♦-2' | 'standard' | { point: 15, isJoker: false } |
| 小王 | 'ddz-SJ' | 'joker' | { point: 16, isJoker: true } |
| 大王 | 'ddz-BJ' | 'joker' | { point: 17, isJoker: true } |
行为
内部使用两个工厂函数:
makeSuitedCard(id, suit, rank):构造花色牌,data = { point, isJoker: false },tags = [suit, rank],value = point;makeJokerCard(id, point):构造王,data = { point, isJoker: true },rank = 'BJ' | 'SJ',tags = ['joker', 'BJ' | 'SJ'],value = point。
示例
import { createDoudizhuCards, doudizhuCardFace } from 'decklet/doudizhu'
const deck = createDoudizhuCards()
console.log(deck.length) // 54
console.log(deck[0]!.id) // 'ddz-♠-3'
console.log(deck[deck.length - 1]!.id) // 'ddz-BJ'
console.log(doudizhuCardFace(deck.find((c) => c.id === 'ddz-♥-A')!)) // '♥A'createShuffledDoudizhuDeck()
function createShuffledDoudizhuDeck(random: RandomProvider): DoudizhuCard[]返回洗牌后的斗地主牌堆。
参数
random: RandomProvider— Engine 的RandomProvider实例。在 Plugin 内部传ctx.random,确定性测试可传SeededRandomProvider。
返回值
DoudizhuCard[]— 洗牌后的 54 张卡牌。相同种子下每次调用得到的洗牌顺序相同。
行为
内部调用 random.shuffle(createDoudizhuCards())。
示例
import {
createShuffledDoudizhuDeck,
SeededRandomProvider,
generateRandomSeed
} from 'decklet/doudizhu'
const seed = generateRandomSeed()
const random = new SeededRandomProvider(seed)
const deck1 = createShuffledDoudizhuDeck(random)
const deck2 = createShuffledDoudizhuDeck(new SeededRandomProvider(seed))
console.log(deck1[0]!.id === deck2[0]!.id) // true(同种子可复现)完整对照表
| 常量 / 类型 | 值 / 类型 | 说明 |
|---|---|---|
DOUDIZHU_DECK_SIZE | 54 | 牌堆总张数 |
DOUDIZHU_HAND_SIZE | 17 | 每人手牌张数 |
DOUDIZHU_BOTTOM_SIZE | 3 | 底牌张数 |
DOUDIZHU_PLAYER_COUNT | 3 | 玩家数量 |
DOUDIZHU_SUITS | ['♠','♥','♣','♦'] as const | 4 种花色 |
DOUDIZHU_RANKS | ['3','4',...,'A','2'] as const | 13 个点数标签 |
DoudizhuSuit | '♠' | '♥' | '♣' | '♦' | 花色字面量类型 |
SMALL_JOKER_POINT | 16 | 小王点数 |
BIG_JOKER_POINT | 17 | 大王点数 |
MIN_CHAIN_POINT | 3 | 可连子最小点数 |
MAX_CHAIN_POINT | 14 | 可连子最大点数(A) |
注意事项
- 斗地主的点数排序(
A=14, 2=15, SJ=16, BJ=17)与扑克不同(扑克中A=14, 2=2),因此斗地主插件使用自己的DoudizhuRankResolver,不复用StandardPokerRankResolver。 - 卡牌
id采用规范格式(如ddz-♠-3、ddz-BJ),同一副牌内稳定可复现——便于回放(ReplayEngine)与测试断言。 asDoudizhuCard的类型守卫基于data对象的鸭子类型检查('point' in data && 'isJoker' in data),不依赖card.type字段。若其它插件的卡牌 payload 恰好也有point与isJoker字段,可能产生误判。createShuffledDoudizhuDeck的洗牌结果完全由传入的RandomProvider决定;在DoudizhuPlugin.setup中传入ctx.random,故 Engine 的种子(new GameEngine({ seed }))决定整局洗牌。