一、游戏背景与规则
十三水(Chinese Poker / 十三张)是起源于中国福建的经典扑克游戏。每位玩家将13张手牌分成三墩:前墩3张、中墩5张、后墩5张。分墩完成后,两两比较对应墩的大小,要求后墩 ≥ 中墩 ≥ 前墩(牌型大小顺序),否则称为”倒水”直接判负。
牌型大小从低到高依次为:单张 < 对子 < 两对 < 三条 < 顺子 < 同花 < 葫芦 < 铁支 < 同花顺。比较时先比牌型,同牌型比最大牌的点数。
十三水的核心算法挑战在于:给定13张牌,如何在满足不”倒水”的约束下,将牌分成三墩使得期望收益最大?这是一个典型的组合优化问题。
二、核心算法原理
2.1 问题建模
13张牌分成3+5+5的三墩,本质上是从13张牌中选3张作为前墩,再从剩余10张中选5张作为中墩,最后5张为后墩。总组合数为 C(13,3) × C(10,5) = 286 × 252 = 72072 种。这个规模完全可以通过暴力枚举求解。
2.2 牌型识别与评估
首先建立牌型识别器,对任意5张或3张牌判断其牌型并计算强度值。强度值设计为一个整数,高位表示牌型等级,低位表示具体牌面大小,便于直接比较。
2.3 贪心优化策略
虽然72072种组合可暴力枚举,但实际游戏中需要快速决策。贪心策略的核心思想是:
1. 优先保证后墩最强(因为它对胜负影响最大)
2. 在剩余牌中优化中墩
3. 最后确定前墩,检查是否倒水
通过预计算所有可能的5张牌型(从13张中选5张有1287种),建立查表机制,可将单次决策时间压缩到毫秒级。
三、Java实现
3.1 牌面表示与基础工具
import java.util.*;
/**
* 十三水核心实现:牌型识别、组合枚举与最优分墩
*/
public class ThirteenWater {
// 牌面点数:2-10, J=11, Q=12, K=13, A=14
public static final String[] RANK_NAMES = {"", "", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"};
public static final String[] SUIT_NAMES = {"♠", "♥", "♣", "♦"}; // 黑桃、红桃、梅花、方块
// 牌型等级常量(用于强度值高位)
public static final int HIGH_CARD = 1;
public static final int ONE_PAIR = 2;
public static final int TWO_PAIR = 3;
public static final int THREE_KIND = 4;
public static final int STRAIGHT = 5;
public static final int FLUSH = 6;
public static final int FULL_HOUSE = 7;
public static final int FOUR_KIND = 8;
public static final int STRAIGHT_FLUSH = 9;
/**
* 单张牌:用整数表示,低4位为点数(2-14),第4-5位为花色(0-3)
*/
public static class Card {
public final int value; // 编码值
public final int rank; // 点数 2-14
public final int suit; // 花色 0-3
public Card(int rank, int suit) {
this.rank = rank;
this.suit = suit;
this.value = (suit << 4) | rank;
}
public static Card fromValue(int v) {
return new Card(v & 0x0F, v >> 4);
}
@Override
public String toString() {
return SUIT_NAMES[suit] + RANK_NAMES[rank];
}
}
/**
* 牌型结果:包含牌型等级和用于比较的关键牌点数
*/
public static class HandRank {
public final int type; // 牌型等级
public final int[] kickers; // 关键牌点数(从大到小),用于同牌型比较
public HandRank(int type, int... kickers) {
this.type = type;
this.kickers = kickers;
}
/**
* 将牌型转换为可比较的强度值
* 高16位:牌型等级
* 低48位:关键牌点数(每8位一张)
*/
public long toStrength() {
long strength = ((long) type) << 48;
for (int i = 0; i < kickers.length && i < 6; i++) {
strength |= ((long) kickers[i]) << (40 - i * 8);
}
return strength;
}
@Override
public String toString() {
String[] typeNames = {"", "高牌", "一对", "两对", "三条", "顺子", "同花", "葫芦", "铁支", "同花顺"};
StringBuilder sb = new StringBuilder(typeNames[type] + "[");
for (int k : kickers) sb.append(RANK_NAMES[k]).append(" ");
return sb.toString().trim() + "]";
}
}
/**
* 解析字符串为牌列表,如 "AS KH QD JC 10S"
*/
public static List<Card> parseHand(String handStr) {
List<Card> cards = new ArrayList<>();
Map<Character, Integer> rankMap = new HashMap<>();
for (int i = 2; i <= 10; i++) rankMap.put((char)('0' + i), i);
rankMap.put('J', 11); rankMap.put('Q', 12); rankMap.put('K', 13); rankMap.put('A', 14);
rankMap.put('j', 11); rankMap.put('q', 12); rankMap.put('k', 13); rankMap.put('a', 14);
Map<Character, Integer> suitMap = Map.of('S', 0, 'H', 1, 'C', 2, 'D', 3,
's', 0, 'h', 1, 'c', 2, 'd', 3,
'♠', 0, '♥', 1, '♣', 2, '♦', 3);
String[] tokens = handStr.split("\\s+");
for (String token : tokens) {
if (token.isEmpty()) continue;
char suitChar = token.charAt(token.length() - 1);
String rankStr = token.substring(0, token.length() - 1);
int rank = rankMap.getOrDefault(rankStr.charAt(0), Integer.parseInt(rankStr));
int suit = suitMap.get(suitChar);
cards.add(new Card(rank, suit));
}
return cards;
}
3.2 牌型识别器
/**
* 识别5张牌的牌型(核心函数)
*/
public static HandRank evaluate5Cards(List<Card> cards) {
if (cards.size() != 5) throw new IllegalArgumentException("需要5张牌");
int[] ranks = cards.stream().mapToInt(c -> c.rank).sorted().toArray();
int[] suits = cards.stream().mapToInt(c -> c.suit).toArray();
// 统计点数频率
Map<Integer, Integer> rankCount = new HashMap<>();
for (int r : ranks) rankCount.merge(r, 1, Integer::sum);
// 按频率分组
List<int[]> groups = new ArrayList<>(); // [点数, 频率]
for (Map.Entry<Integer, Integer> e : rankCount.entrySet()) {
groups.add(new int[]{e.getKey(), e.getValue()});
}
groups.sort((a, b) -> b[1] != a[1] ? b[1] - a[1] : b[0] - a[0]); // 频率降序,同频率点数降序
boolean isFlush = true;
for (int i = 1; i < 5; i++) if (suits[i] != suits[0]) { isFlush = false; break; }
boolean isStraight = true;
for (int i = 1; i < 5; i++) if (ranks[i] != ranks[i-1] + 1) { isStraight = false; break; }
// 特殊顺子:A-2-3-4-5
boolean isWheel = (ranks[0] == 2 && ranks[1] == 3 && ranks[2] == 4 && ranks[3] == 5 && ranks[4] == 14);
if (isWheel) isStraight = true;
// 同花顺
if (isFlush && isStraight) {
int high = isWheel ? 5 : ranks[4];
return new HandRank(STRAIGHT_FLUSH, high);
}
// 铁支
if (groups.get(0)[1] == 4) {
return new HandRank(FOUR_KIND, groups.get(0)[0], groups.get(1)[0]);
}
// 葫芦
if (groups.get(0)[1] == 3 && groups.get(1)[1] >= 2) {
return new HandRank(FULL_HOUSE, groups.get(0)[0], groups.get(1)[0]);
}
// 同花
if (isFlush) {
return new HandRank(FLUSH, ranks[4], ranks[3], ranks[2], ranks[1], ranks[0]);
}
// 顺子
if (isStraight) {
int high = isWheel ? 5 : ranks[4];
return new HandRank(STRAIGHT, high);
}
// 三条
if (groups.get(0)[1] == 3) {
return new HandRank(THREE_KIND, groups.get(0)[0], groups.get(1)[0], groups.get(2)[0]);
}
// 两对
if (groups.get(0)[1] == 2 && groups.get(1)[1] == 2) {
return new HandRank(TWO_PAIR, groups.get(0)[0], groups.get(1)[0], groups.get(2)[0]);
}
// 一对
if (groups.get(0)[1] == 2) {
return new HandRank(ONE_PAIR, groups.get(0)[0], groups.get(1)[0], groups.get(2)[0], groups.get(3)[0]);
}
// 高牌
return new HandRank(HIGH_CARD, ranks[4], ranks[3], ranks[2], ranks[1], ranks[0]);
}
/**
* 识别3张牌的牌型(前墩特殊规则)
*/
public static HandRank evaluate3Cards(List<Card> cards) {
if (cards.size() != 3) throw new IllegalArgumentException("需要3张牌");
int[] ranks = cards.stream().mapToInt(c -> c.rank).sorted().toArray();
Map<Integer, Integer> rankCount = new HashMap<>();
for (int r : ranks) rankCount.merge(r, 1, Integer::sum);
List<int[]> groups = new ArrayList<>();
for (Map.Entry<Integer, Integer> e : rankCount.entrySet()) {
groups.add(new int[]{e.getKey(), e.getValue()});
}
groups.sort((a, b) -> b[1] != a[1] ? b[1] - a[1] : b[0] - a[0]);
if (groups.get(0)[1] == 3) {
return new HandRank(THREE_KIND, groups.get(0)[0]);
}
if (groups.get(0)[1] == 2) {
return new HandRank(ONE_PAIR, groups.get(0)[0], groups.get(1)[0]);
}
return new HandRank(HIGH_CARD, ranks[2], ranks[1], ranks[0]);
}
3.3 最优分墩求解器
/**
* 三墩分法结果
*/
public static class Division {
public final List<Card> front; // 前墩3张
public final List<Card> middle; // 中墩5张
public final List<Card> back; // 后墩5张
public final HandRank frontRank;
public final HandRank middleRank;
public final HandRank backRank;
public final long totalStrength; // 综合强度(用于比较)
public final boolean isValid; // 是否不倒水
public Division(List<Card> front, List<Card> middle, List<Card> back) {
this.front = front;
this.middle = middle;
this.back = back;
this.frontRank = evaluate3Cards(front);
this.middleRank = evaluate5Cards(middle);
this.backRank = evaluate5Cards(back);
this.isValid = backRank.toStrength() >= middleRank.toStrength() &&
middleRank.toStrength() >= frontRank.toStrength();
// 综合强度:后墩权重最高,中墩次之,前墩最低
this.totalStrength = (backRank.toStrength() << 32) |
(middleRank.toStrength() << 16) |
(frontRank.toStrength() & 0xFFFF);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(isValid ? "【有效分墩】" : "【倒水!无效】").append("\n");
sb.append("前墩(3张): ").append(front).append(" -> ").append(frontRank).append("\n");
sb.append("中墩(5张): ").append(middle).append(" -> ").append(middleRank).append("\n");
sb.append("后墩(5张): ").append(back).append(" -> ").append(backRank).append("\n");
sb.append(String.format("综合强度: %016X\n", totalStrength));
return sb.toString();
}
}
/**
* 穷举所有合法分墩,返回最优解
* 时间复杂度:C(13,3) × C(10,5) = 72072 种组合
*/
public static Division findOptimalDivision(List<Card> hand) {
if (hand.size() != 13) throw new IllegalArgumentException("需要13张牌");
Division best = null;
long bestStrength = -1;
int validCount = 0;
// 枚举前墩3张牌的所有组合
List<int[]> frontCombinations = generateCombinations(13, 3);
for (int[] frontIdx : frontCombinations) {
List<Card> front = new ArrayList<>();
boolean[] used = new boolean[13];
for (int idx : frontIdx) {
front.add(hand.get(idx));
used[idx] = true;
}
// 从剩余10张中枚举中墩5张
List<Integer> remaining = new ArrayList<>();
for (int i = 0; i < 13; i++) if (!used[i]) remaining.add(i);
List<int[]> middleCombinations = generateCombinations(10, 5);
for (int[] midIdx : middleCombinations) {
List<Card> middle = new ArrayList<>();
List<Card> back = new ArrayList<>();
boolean[] midUsed = new boolean[10];
for (int idx : midIdx) {
middle.add(hand.get(remaining.get(idx)));
midUsed[idx] = true;
}
for (int i = 0; i < 10; i++) {
if (!midUsed[i]) back.add(hand.get(remaining.get(i)));
}
Division div = new Division(front, middle, back);
if (div.isValid) {
validCount++;
if (div.totalStrength > bestStrength) {
bestStrength = div.totalStrength;
best = div;
}
}
}
}
System.out.println("枚举完成:总组合 " + (frontCombinations.size() * 252) +
",合法分墩 " + validCount);
return best;
}
/**
* 生成从n个元素中选k个的所有组合(索引组合)
*/
private static List<int[]> generateCombinations(int n, int k) {
List<int[]> result = new ArrayList<>();
int[] combo = new int[k];
for (int i = 0; i < k; i++) combo[i] = i;
while (true) {
result.add(combo.clone());
// 找到最右边可以增大的位置
int i = k - 1;
while (i >= 0 && combo[i] == n - k + i) i--;
if (i < 0) break;
combo[i]++;
for (int j = i + 1; j < k; j++) combo[j] = combo[j - 1] + 1;
}
return result;
}
3.4 快速贪心算法与对战模拟
/**
* 贪心快速分墩:优先构建强后墩,再优化中墩和前墩
* 适用于需要毫秒级响应的场景
*/
public static Division greedyDivision(List<Card> hand) {
List<Card> remaining = new ArrayList<>(hand);
// Step 1: 从13张中选最强的5张作为后墩候选
List<Card> bestBack = null;
HandRank bestBackRank = null;
long bestBackStr = -1;
List<int[]> backCombos = generateCombinations(13, 5);
for (int[] idx : backCombos) {
List<Card> candidate = new ArrayList<>();
for (int i : idx) candidate.add(hand.get(i));
HandRank rank = evaluate5Cards(candidate);
long str = rank.toStrength();
if (str > bestBackStr) {
bestBackStr = str;
bestBack = candidate;
bestBackRank = rank;
}
}
// Step 2: 从剩余8张中选最强的5张作为中墩
List<Integer> backIdx = new ArrayList<>();
for (Card c : bestBack) backIdx.add(hand.indexOf(c));
List<Card> afterBack = new ArrayList<>();
for (int i = 0; i < 13; i++) if (!backIdx.contains(i)) afterBack.add(hand.get(i));
List<Card> bestMiddle = null;
HandRank bestMidRank = null;
long bestMidStr = -1;
List<int[]> midCombos = generateCombinations(8, 5);
for (int[] idx : midCombos) {
List<Card> candidate = new ArrayList<>();
for (int i : idx) candidate.add(afterBack.get(i));
HandRank rank = evaluate5Cards(candidate);
long str = rank.toStrength();
if (str > bestMidStr) {
bestMidStr = str;
bestMiddle = candidate;
bestMidRank = rank;
}
}
// Step 3: 剩余3张为前墩
List<Card> front = new ArrayList<>();
for (Card c : afterBack) if (!bestMiddle.contains(c)) front.add(c);
Division div = new Division(front, bestMiddle, bestBack);
// 如果倒水,尝试调整: weakening 后墩或 strengthening 中墩
if (!div.isValid) {
return findOptimalDivision(hand); // 回退到精确求解
}
return div;
}
/**
* 两玩家对战:比较三墩,计算胜负
* @return 净胜墩数(正表示div1赢)
*/
public static int compareDivisions(Division d1, Division d2) {
int score = 0;
long f1 = d1.frontRank.toStrength(), f2 = d2.frontRank.toStrength();
long m1 = d1.middleRank.toStrength(), m2 = d2.middleRank.toStrength();
long b1 = d1.backRank.toStrength(), b2 = d2.backRank.toStrength();
if (f1 > f2) score++; else if (f1 < f2) score--;
if (m1 > m2) score++; else if (m1 < m2) score--;
if (b1 > b2) score++; else if (b1 < b2) score--;
// 特殊奖励:三墩全赢(打枪)额外+3
if (score == 3) score += 3;
if (score == -3) score -= 3;
return score;
}
public static void main(String[] args) {
System.out.println("=== 十三水最优分墩求解器 ===\n");
// 测试手牌1:强牌
String hand1 = "AS KS QS JS 10S 9H 9D 9C 8H 7D 6C 5S 4D";
List<Card> cards1 = parseHand(hand1);
System.out.println("手牌1: " + cards1);
System.out.println("--- 精确最优解 ---");
Division opt1 = findOptimalDivision(cards1);
System.out.println(opt1);
// 测试手牌2:中等牌力
String hand2 = "AH KH QD JC 10H 9S 8D 7C 6H 5D 4C 3H 2D";
List<Card> cards2 = parseHand(hand2);
System.out.println("\n手牌2: " + cards2);
System.out.println("--- 精确最优解 ---");
Division opt2 = findOptimalDivision(cards2);
System.out.println(opt2);
// 测试手牌3:随机生成对比
System.out.println("\n--- 贪心 vs 精确对比 ---");
long start1 = System.nanoTime();
Division greedy = greedyDivision(cards1);
long time1 = System.nanoTime() - start1;
long start2 = System.nanoTime();
Division exact = findOptimalDivision(cards1);
long time2 = System.nanoTime() - start2;
System.out.println("贪心算法耗时: " + (time1 / 1_000_000.0) + "ms");
System.out.println("精确算法耗时: " + (time2 / 1_000_000.0) + "ms");
System.out.println("贪心结果: " + greedy.backRank + " / " + greedy.middleRank + " / " + greedy.frontRank);
System.out.println("精确结果: " + exact.backRank + " / " + exact.middleRank + " / " + exact.frontRank);
// 对战模拟
if (opt1 != null && opt2 != null) {
System.out.println("\n--- 对战模拟:手牌1 vs 手牌2 ---");
int result = compareDivisions(opt1, opt2);
System.out.println("手牌1净胜墩数: " + result);
}
}
}
四、复杂度分析
| 模块 | 时间复杂度 | 说明 |
|---|---|---|
| 5张牌型识别 | O(1) | 固定5张,常数时间 |
| 3张牌型识别 | O(1) | 固定3张,常数时间 |
| 精确最优分墩 | O(C(13,3)·C(10,5)) = O(72072) | 枚举所有分法 |
| 贪心快速分墩 | O(C(13,5)+C(8,5)) = O(1300) | 分阶段枚举 |
| 两玩家对战 | O(1) | 三墩比较 |
精确算法在普通PC上执行时间约10-50ms,贪心算法在1ms以内,均可满足实时游戏需求。
五、总结
本文完整实现了十三水扑克游戏的Java版核心算法,涵盖牌型识别、组合枚举与最优分墩策略:
- 牌型编码:用紧凑整数编码表示牌和牌型,支持O(1)时间比较大小
- 牌型识别器:完整支持9种牌型的精确识别,包括A-2-3-4-5特殊顺子
- 精确求解:枚举72072种分墩组合,保证找到全局最优不”倒水”方案
- 贪心加速:分阶段优先构建强后墩,将复杂度降低两个数量级
- 对战引擎:支持两玩家分墩结果比较,含”打枪”特殊计分规则
读者可在此基础上接入随机发牌模块、多玩家循环对战和牌力统计分析,构建完整的十三水AI训练平台。贪心策略也可进一步改进为带剪枝的启发式搜索,在保持速度的同时更接近精确最优解。