每日算法 — 使用java实现黑桃皇后:贪心策略与卡牌规避算法

引言

黑桃皇后(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水平。