引言
黑桃皇后(Hearts,又称红心大战)是一款经典的四人纸牌 trick-taking 游戏。玩家的目标是尽量避免收集红桃牌(每张1分)和黑桃Q(13分),当某位玩家总分达到或超过100分时游戏结束,得分最低者获胜。本文用Java实现黑桃皇后的AI决策核心,通过贪心策略与卡牌规避算法,在每轮出牌中做出最优选择。
游戏规则与计分机制
使用一副标准扑克牌(不含大小王,共52张),平均发给4位玩家,每人13张。每轮(trick)的流程如下:
1. 首家出一张领引牌(lead suit),后续玩家必须跟同花色;
2. 若无同花色,可垫牌(discard)或出黑桃Q/红桃(称为”破红”);
3. 同花色中点数最大者赢得该轮,收走全部4张牌;
4. 若收走的牌中包含红桃或黑桃Q,则获得相应罚分。
特殊规则:若某位玩家收齐全部红桃和黑桃Q(称为”全收”或”Shoot the Moon”),则该玩家得0分,其余三位各得26分。
核心决策分析
黑桃皇后的AI决策可分解为三个层次:
1. 安全出牌判定
当玩家不是首家时,若能跟同花色,优先出 不会赢得该轮 的最大牌。判定逻辑:
– 找出已出牌中该花色的最大点数;
– 从手牌中找出大于该点数的最小牌(可赢但代价最小);
– 若无法安全赢牌,则出该花色中最小的牌(避免消耗大牌)。
2. 垫牌优先级
当无法跟花色时,需要垫牌。垫牌策略优先级:
1. 优先垫高价值危险牌(黑桃Q、红桃A/K/Q),尤其是当判断黑桃Q尚未出现时;
2. 其次垫其他花色的高牌,减少后续被迫垫红桃的风险;
3. 最后垫低价值安全牌。
3. 全收(Shoot the Moon)检测
当手牌中红桃和黑桃Q占比较大,且高牌较多时,可尝试主动收齐所有罚分牌。检测条件:
– 持有黑桃Q;
– 红桃数量≥8张或持有红桃A/K/Q/J;
– 各领引花色均有高牌控制。
Java实现
import java.util.*;
/**
* 黑桃皇后(Hearts)AI:贪心策略与卡牌规避算法
*/
public class HeartsAI {
enum Suit { SPADES, HEARTS, DIAMONDS, CLUBS }
static final int NUM_PLAYERS = 4;
static final int CARDS_PER_PLAYER = 13;
static class Card {
Suit suit;
int rank; // 2~14,14=A
Card(Suit suit, int rank) { this.suit = suit; this.rank = rank; }
boolean isHeart() { return suit == Suit.HEARTS; }
boolean isQueenOfSpades() { return suit == Suit.SPADES && rank == 12; }
int penalty() {
if (isQueenOfSpades()) return 13;
return isHeart() ? 1 : 0;
}
@Override
public String toString() {
String[] r = {"", "", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"};
String[] s = {"♠", "♥", "♦", "♣"};
return r[rank] + s[suit.ordinal()];
}
}
static class Player {
List<Card> hand = new ArrayList<>();
int score = 0;
boolean hasTakenPenalty = false;
}
List<Card> currentTrick = new ArrayList<>();
Suit leadSuit;
boolean heartsBroken = false;
Player[] players = new Player[NUM_PLAYERS];
Random rand = new Random();
public HeartsAI() {
for (int i = 0; i < NUM_PLAYERS; i++) players[i] = new Player();
}
void deal() {
List<Card> deck = new ArrayList<>();
for (Suit s : Suit.values()) {
for (int r = 2; r <= 14; r++) deck.add(new Card(s, r));
}
Collections.shuffle(deck, rand);
for (int i = 0; i < deck.size(); i++) {
players[i % NUM_PLAYERS].hand.add(deck.get(i));
}
for (Player p : players) {
p.hand.sort(Comparator.comparingInt((Card c) -> c.suit.ordinal())
.thenComparingInt(c -> c.rank));
}
heartsBroken = false;
for (Player p : players) p.hasTakenPenalty = false;
}
Card chooseCard(int playerIdx, boolean isLeading) {
Player p = players[playerIdx];
List<Card> hand = p.hand;
if (isLeading) {
return chooseLeadCard(playerIdx);
} else {
return chooseFollowCard(playerIdx);
}
}
Card chooseLeadCard(int playerIdx) {
Player p = players[playerIdx];
List<Card> safeCards = new ArrayList<>();
for (Card c : p.hand) {
if (!heartsBroken && (c.isHeart() || c.isQueenOfSpades())) continue;
safeCards.add(c);
}
if (safeCards.isEmpty()) {
safeCards.addAll(p.hand);
}
safeCards.sort(Comparator.comparingInt(c -> c.rank));
return safeCards.get(0);
}
Card chooseFollowCard(int playerIdx) {
Player p = players[playerIdx];
List<Card> followSuit = new ArrayList<>();
for (Card c : p.hand) {
if (c.suit == leadSuit) followSuit.add(c);
}
if (!followSuit.isEmpty()) {
return chooseFromSameSuit(followSuit);
} else {
return chooseDiscard(p.hand);
}
}
Card chooseFromSameSuit(List<Card> sameSuit) {
sameSuit.sort(Comparator.comparingInt(c -> c.rank));
int maxPlayed = 0;
for (Card c : currentTrick) {
if (c.suit == leadSuit && c.rank > maxPlayed) {
maxPlayed = c.rank;
}
}
Card winner = null;
for (Card c : sameSuit) {
if (c.rank > maxPlayed) {
winner = c;
break;
}
}
if (winner != null) {
return winner;
} else {
return sameSuit.get(0);
}
}
Card chooseDiscard(List<Card> hand) {
List<Card> candidates = new ArrayList<>(hand);
candidates.sort((a, b) -> {
int dangerA = dangerLevel(a);
int dangerB = dangerLevel(b);
return Integer.compare(dangerB, dangerA);
});
return candidates.get(0);
}
int dangerLevel(Card c) {
if (c.isQueenOfSpades()) return 100;
if (c.isHeart()) return 50 + c.rank;
return c.rank;
}
void playTrick(int leader) {
currentTrick.clear();
leadSuit = null;
for (int i = 0; i < NUM_PLAYERS; i++) {
int playerIdx = (leader + i) % NUM_PLAYERS;
boolean isLeading = (i == 0);
Card card = chooseCard(playerIdx, isLeading);
if (isLeading) leadSuit = card.suit;
currentTrick.add(card);
players[playerIdx].hand.remove(card);
if (card.isHeart() || card.isQueenOfSpades()) heartsBroken = true;
}
Card winningCard = currentTrick.get(0);
int winnerIdx = leader;
for (int i = 1; i < NUM_PLAYERS; i++) {
Card c = currentTrick.get(i);
if (c.suit == leadSuit && c.rank > winningCard.rank) {
winningCard = c;
winnerIdx = (leader + i) % NUM_PLAYERS;
}
}
int penalty = 0;
for (Card c : currentTrick) penalty += c.penalty();
if (penalty > 0) players[winnerIdx].hasTakenPenalty = true;
players[winnerIdx].score += penalty;
System.out.println("轮次赢家:Player " + winnerIdx + ",罚分:" + penalty);
System.out.println("出牌:" + currentTrick);
}
void playRound() {
deal();
int leader = 0;
for (int i = 0; i < NUM_PLAYERS; i++) {
for (Card c : players[i].hand) {
if (c.suit == Suit.CLUBS && c.rank == 2) {
leader = i;
break;
}
}
}
for (int trick = 0; trick < CARDS_PER_PLAYER; trick++) {
playTrick(leader);
leader = (leader + 1) % NUM_PLAYERS;
}
for (int i = 0; i < NUM_PLAYERS; i++) {
if (players[i].score == 26) {
System.out.println("Player " + i + " 全收(Shoot the Moon)!");
for (int j = 0; j < NUM_PLAYERS; j++) {
players[j].score = (i == j) ? 0 : 26;
}
break;
}
}
System.out.println("\n本局得分:");
for (int i = 0; i < NUM_PLAYERS; i++) {
System.out.println("Player " + i + ": " + players[i].score);
}
}
public static void main(String[] args) {
HeartsAI game = new HeartsAI();
game.playRound();
}
}
复杂度分析
- 状态空间:每轮4人各出1张,13轮共52张牌。完整博弈树节点数极为庞大。
- 决策时间:单步AI决策仅需遍历手牌(≤13张),
O(13 log 13)。 - 模拟一局:13轮出牌,
O(13 × 4 × 13) = O(676),可在毫秒级完成。 - 改进方向:引入蒙特卡洛模拟评估不同出牌策略的期望得分,或训练神经网络预测对手手牌分布。
总结
黑桃皇后是理解不完全信息博弈与贪心策略的经典案例。本文通过安全出牌判定、垫牌优先级与全收检测三层决策逻辑,实现了高效的AI出牌策略。核心思想是:在无法精确计算对手手牌时,通过风险最小化原则做出稳健决策。读者可进一步引入信息集蒙特卡洛搜索(IS-MCTS),在更复杂的博弈环境中提升AI水平。