游戏简介与算法背景
大老二(Big Two)是一款风靡亚洲的经典扑克游戏,通常由4人参与,使用一副去掉大小王的52张牌。游戏目标是尽快打出手中的所有牌,先出完者获胜。牌型大小规则独特:黑桃2最大,红桃2次之,方块2再次,梅花2最小。出牌必须遵循”大压小”原则,且牌型必须匹配上家(单张对单张、对子对对子、顺子对顺子、同花顺对同花顺等)。
从算法角度看,大老二是不完全信息动态博弈的典型代表,核心挑战在于:手牌评估、牌型识别、出牌决策以及对手建模。本文聚焦可计算的确定性策略——牌型排序评估与贪心最优出牌算法。
核心算法设计
1. 牌型识别与排序
将52张牌编码为 0-51,按大老二规则定义比较器:
– 点数优先级:2 > A > K > Q > J > 10 > 9 > 8 > 7 > 6 > 5 > 4 > 3
– 花色优先级:黑桃 > 红桃 > 方块 > 梅花
牌型分类:单张、对子、三条、顺子(5张连续)、同花(5张同花色)、葫芦(3+2)、四条(4+1)、同花顺。
2. 手牌价值评估
为每个可出牌型计算”牌力值”,综合以下因素:
– 牌型本身大小(大牌型优先出)
– 出牌后手牌的可出牌型数量(保留灵活性)
– 控场能力(是否保留最大牌)
3. 贪心出牌策略
在必须跟牌时,选择能压过对方且牌力最小的牌型;在自由出牌时,优先打出”限制性最强”的牌型(即其他玩家难以接上的牌)。
Java 完整实现
import java.util.*;
/**
* 大老二游戏 — 牌型排序评估与贪心最优出牌策略
*
* 核心算法:
* 1. 牌型识别:单张/对子/三条/顺子/同花/葫芦/四条/同花顺
* 2. 手牌评估函数:综合牌力、灵活性、控场能力
* 3. 贪心出牌:最小可压牌型跟牌,限制性最强牌型首发
* 4. 牌型枚举与排序:组合生成 + 规则过滤
*/
public class BigTwo {
// 点数:3=0, 4=1, ..., 10=7, J=8, Q=9, K=10, A=11, 2=12
private static final String[] RANK_NAMES = {"3","4","5","6","7","8","9","10","J","Q","K","A","2"};
// 花色:梅花=0, 方块=1, 红桃=2, 黑桃=3
private static final String[] SUIT_NAMES = {"♣","♦","♥","♠"};
// 牌型枚举
enum HandType {
SINGLE, // 单张
PAIR, // 对子
TRIPLE, // 三条
STRAIGHT, // 顺子(5张连续)
FLUSH, // 同花
FULL_HOUSE, // 葫芦
FOUR_KIND, // 四条
STRAIGHT_FLUSH // 同花顺
}
/**
* 单张牌:编码 0-51,数值越大牌越大
*/
static class Card implements Comparable<Card> {
final int code; // 0-51
final int rank; // 0-12
final int suit; // 0-3
Card(int code) {
this.code = code;
this.rank = code / 4;
this.suit = code % 4;
}
@Override
public int compareTo(Card other) {
return Integer.compare(this.code, other.code);
}
@Override
public String toString() {
return SUIT_NAMES[suit] + RANK_NAMES[rank];
}
}
/**
* 出牌组合:一组可一起出的牌
*/
static class CardCombo implements Comparable<CardCombo> {
final List<Card> cards;
final HandType type;
final int strength; // 牌型强度,用于比较大小
CardCombo(List<Card> cards, HandType type) {
this.cards = new ArrayList<>(cards);
this.type = type;
this.strength = calculateStrength();
}
/**
* 计算牌型强度:编码为整数便于比较
* 高16位:牌型等级,低48位:牌面编码
*/
private int calculateStrength() {
cards.sort(Comparator.reverseOrder()); // 从大到小排序
int typeValue = type.ordinal() * 1000000;
int cardValue = 0;
for (int i = 0; i < cards.size(); i++) {
cardValue += cards.get(i).code * Math.pow(52, cards.size() - 1 - i);
}
return typeValue + cardValue;
}
boolean beats(CardCombo other) {
if (other == null) return true; // 自由出牌
if (this.type != other.type) {
// 只有同花顺 > 四条 > 葫芦 > 同花 > 顺子,且必须5张
return this.type.ordinal() > other.type.ordinal()
&& this.cards.size() == other.cards.size();
}
return this.strength > other.strength;
}
@Override
public int compareTo(CardCombo other) {
return Integer.compare(this.strength, other.strength);
}
@Override
public String toString() {
return type + cards.toString();
}
}
/**
* 枚举手牌中所有合法的出牌组合
*/
public static List<CardCombo> enumerateCombos(List<Card> hand) {
List<CardCombo> combos = new ArrayList<>();
// 1. 单张
for (Card c : hand) {
combos.add(new CardCombo(Collections.singletonList(c), HandType.SINGLE));
}
// 2. 对子、三条、四条
Map<Integer, List<Card>> rankGroups = new HashMap<>();
for (Card c : hand) {
rankGroups.computeIfAbsent(c.rank, k -> new ArrayList<>()).add(c);
}
for (List<Card> group : rankGroups.values()) {
if (group.size() >= 2) {
// 对子:C(n,2)
for (int i = 0; i < group.size(); i++) {
for (int j = i + 1; j < group.size(); j++) {
combos.add(new CardCombo(Arrays.asList(group.get(i), group.get(j)), HandType.PAIR));
}
}
}
if (group.size() >= 3) {
// 三条:C(n,3)
List<Card> triple = group.subList(0, 3);
combos.add(new CardCombo(triple, HandType.TRIPLE));
}
if (group.size() == 4) {
combos.add(new CardCombo(group, HandType.FOUR_KIND));
}
}
// 3. 顺子(5张连续点数)
List<Card> sortedHand = new ArrayList<>(hand);
sortedHand.sort(Comparator.naturalOrder());
for (int start = 0; start <= 8; start++) { // 顺子起点最多到10(即rank 8)
findStraights(sortedHand, start, 5, new ArrayList<>(), combos);
}
// 4. 同花(5张同花色)
Map<Integer, List<Card>> suitGroups = new HashMap<>();
for (Card c : hand) {
suitGroups.computeIfAbsent(c.suit, k -> new ArrayList<>()).add(c);
}
for (List<Card> suitCards : suitGroups.values()) {
if (suitCards.size() >= 5) {
generateCombinations(suitCards, 5, new ArrayList<>(), 0, (combo) -> {
// 检查是否同时为顺子(同花顺)
List<Card> sorted = new ArrayList<>(combo);
sorted.sort(Comparator.naturalOrder());
boolean isStraight = true;
for (int i = 1; i < 5; i++) {
if (sorted.get(i).rank != sorted.get(i-1).rank + 1) {
isStraight = false;
break;
}
}
if (isStraight) {
combos.add(new CardCombo(combo, HandType.STRAIGHT_FLUSH));
} else {
combos.add(new CardCombo(combo, HandType.FLUSH));
}
});
}
}
// 5. 葫芦(3+2)
for (List<Card> tripleGroup : rankGroups.values()) {
if (tripleGroup.size() >= 3) {
for (List<Card> pairGroup : rankGroups.values()) {
if (pairGroup.size() >= 2 && pairGroup.get(0).rank != tripleGroup.get(0).rank) {
List<Card> fullHouse = new ArrayList<>();
fullHouse.addAll(tripleGroup.subList(0, 3));
fullHouse.addAll(pairGroup.subList(0, 2));
combos.add(new CardCombo(fullHouse, HandType.FULL_HOUSE));
}
}
}
}
// 6. 四条带单张
for (List<Card> fourGroup : rankGroups.values()) {
if (fourGroup.size() == 4) {
for (Card kicker : hand) {
if (kicker.rank != fourGroup.get(0).rank) {
List<Card> fourKind = new ArrayList<>(fourGroup);
fourKind.add(kicker);
combos.add(new CardCombo(fourKind, HandType.FOUR_KIND));
}
}
}
}
return combos;
}
/**
* 递归查找所有顺子组合
*/
private static void findStraights(List<Card> hand, int startRank, int need,
List<Card> current, List<CardCombo> results) {
if (need == 0) {
results.add(new CardCombo(new ArrayList<>(current), HandType.STRAIGHT));
return;
}
int targetRank = startRank + (5 - need);
for (Card c : hand) {
if (c.rank == targetRank) {
// 避免同一位置的牌重复使用
if (current.isEmpty() || c.code > current.get(current.size() - 1).code) {
current.add(c);
findStraights(hand, startRank, need - 1, current, results);
current.remove(current.size() - 1);
}
}
}
}
/**
* 通用组合生成器
*/
private static void generateCombinations(List<Card> cards, int k, List<Card> current,
int start, java.util.function.Consumer<List<Card>> consumer) {
if (current.size() == k) {
consumer.accept(new ArrayList<>(current));
return;
}
for (int i = start; i < cards.size(); i++) {
current.add(cards.get(i));
generateCombinations(cards, k, current, i + 1, consumer);
current.remove(current.size() - 1);
}
}
/**
* 贪心出牌策略
* @param hand 当前手牌
* @param lastCombo 上家出的牌(null表示自由出牌)
* @return 推荐的出牌组合
*/
public static CardCombo greedyPlay(List<Card> hand, CardCombo lastCombo) {
List<CardCombo> allCombos = enumerateCombos(hand);
if (lastCombo == null) {
// 自由出牌:选择"限制性最强"的牌型
// 策略优先级:大牌型优先,同时保留小牌型灵活性
return selectOpeningPlay(allCombos, hand);
} else {
// 必须跟牌:选择能压过对方的最小牌型
CardCombo best = null;
for (CardCombo combo : allCombos) {
if (combo.beats(lastCombo)) {
if (best == null || combo.strength < best.strength) {
best = combo;
}
}
}
return best; // 可能返回null(无牌可出)
}
}
/**
* 首发出牌策略:优先打出难以被接的牌型
*/
private static CardCombo selectOpeningPlay(List<CardCombo> combos, List<Card> hand) {
if (combos.isEmpty()) return null;
// 评估每个可选出牌的价值
CardCombo best = null;
double bestScore = Double.NEGATIVE_INFINITY;
for (CardCombo combo : combos) {
double score = evaluateOpening(combo, hand);
if (score > bestScore) {
bestScore = score;
best = combo;
}
}
return best;
}
/**
* 评估首发出牌的价值
* 高分因素:牌型大(难被接)、出牌后手牌灵活、不浪费大牌控场能力
*/
private static double evaluateOpening(CardCombo combo, List<Card> hand) {
double score = 0;
// 1. 牌型基础分:大牌型更难被接,但得分不能过高以免浪费
score += combo.type.ordinal() * 10;
// 2. 控场惩罚:如果手中有最大牌(黑桃2、红桃2等),尽量保留
boolean hasBig2 = hand.stream().anyMatch(c -> c.code >= 50); // 2的牌
boolean comboHasBig2 = combo.cards.stream().anyMatch(c -> c.code >= 50);
if (hasBig2 && comboHasBig2) {
score -= 50; // 惩罚过早打出2
}
// 3. 灵活性奖励:出牌后剩余手牌的可出牌型数量
List<Card> remaining = new ArrayList<>(hand);
remaining.removeAll(combo.cards);
List<CardCombo> futureCombos = enumerateCombos(remaining);
score += futureCombos.size() * 0.5;
// 4. 小牌清理奖励:优先打出3、4等小单张
boolean hasSmallCards = combo.cards.stream().anyMatch(c -> c.rank <= 2);
if (hasSmallCards && combo.type == HandType.SINGLE) {
score += 20; // 鼓励尽早出掉小单张
}
return score;
}
/**
* 发牌并模拟一局
*/
public static void simulateGame() {
// 生成一副牌
List<Card> deck = new ArrayList<>();
for (int i = 0; i < 52; i++) {
deck.add(new Card(i));
}
Collections.shuffle(deck);
// 4人各发13张
List<List<Card>> players = new ArrayList<>();
for (int p = 0; p < 4; p++) {
List<Card> hand = new ArrayList<>(deck.subList(p * 13, (p + 1) * 13));
hand.sort(Comparator.naturalOrder());
players.add(hand);
}
System.out.println("=== 大老二贪心策略模拟 ===\n");
for (int p = 0; p < 4; p++) {
System.out.printf("玩家%d手牌: %s%n", p + 1, players.get(p));
}
System.out.println();
// 找到持有梅花3的玩家先出牌
int currentPlayer = -1;
for (int p = 0; p < 4; p++) {
for (Card c : players.get(p)) {
if (c.rank == 0 && c.suit == 0) { // 梅花3
currentPlayer = p;
break;
}
}
if (currentPlayer != -1) break;
}
System.out.printf("玩家%d持有梅花3,首发出牌!%n%n", currentPlayer + 1);
// 模拟几轮
CardCombo lastCombo = null;
int passes = 0;
int turn = 0;
while (turn < 20) { // 最多模拟20轮
List<Card> hand = players.get(currentPlayer);
if (hand.isEmpty()) {
System.out.printf("玩家%d已出完所有牌!%n", currentPlayer + 1);
break;
}
CardCombo play = greedyPlay(hand, lastCombo);
if (play != null) {
hand.removeAll(play.cards);
System.out.printf("玩家%d 出牌: %s (剩余%d张)%n",
currentPlayer + 1, play, hand.size());
lastCombo = play;
passes = 0;
} else {
System.out.printf("玩家%d 过牌%n", currentPlayer + 1);
passes++;
}
// 3人连续过牌,最后一出牌者重新获得出牌权
if (passes >= 3) {
System.out.println("--- 本轮结束,重新获得出牌权 ---\n");
lastCombo = null;
passes = 0;
}
currentPlayer = (currentPlayer + 1) % 4;
turn++;
}
System.out.println("\n=== 最终手牌 ===");
for (int p = 0; p < 4; p++) {
System.out.printf("玩家%d剩余: %d张 %s%n",
p + 1, players.get(p).size(), players.get(p));
}
}
public static void main(String[] args) {
simulateGame();
}
}
算法复杂度分析
| 模块 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 牌型枚举 | O(2^n) 最坏 |
O(C(n,5)) |
n为手牌数(通常13张,可接受) |
| 单张/对子/三条 | O(n²) |
O(n²) |
按点数分组后组合 |
| 顺子枚举 | O(n^5) 最坏 |
O(n^5) |
实际受连续点数限制,远小于理论值 |
| 同花枚举 | O(C(13,5)) |
O(C(13,5)) |
同花色最多13张 |
| 贪心决策 | O(m) |
O(1) |
m为可选出牌数 |
对于13张手牌,完整枚举通常在毫秒级完成。
扩展思考
- 蒙特卡洛树搜索(MCTS):将大老二建模为不完美信息博弈,使用信息集 MCTS 进行决策,可显著超越贪心策略。
- 对手建模:通过已出牌历史推断对手手牌分布,使用贝叶斯更新调整出牌策略(例如判断某人是否持有2)。
- 深度强化学习:训练神经网络评估函数
V(hand),结合策略网络π(hand, last)输出出牌概率分布,类似 AlphaZero 范式。 - 合作与欺骗:在多人博弈中,有时需要”放水”让弱势玩家接牌以阻止领先者获胜,引入复杂的联盟形成与背叛机制。
总结
大老二游戏的算法实现展示了组合博弈中的核心挑战:在巨大的状态空间中快速评估并决策。牌型枚举与排序是基础,贪心策略提供了可解释的基准方案,而更强的算法需要引入对手建模与搜索。从工程角度看,高效的牌型识别和数据结构设计(如按点数/花色预分组)是性能关键,这一思路同样适用于斗地主、麻将等其他扑克类游戏的AI开发。