UNO是全球流行的经典卡牌游戏,玩家通过轮流出牌,率先打光手牌者获胜。本文用Java实现带AI的UNO游戏核心逻辑,重点讲解贪心出牌策略、颜色匹配决策与手牌评估函数设计。
游戏规则建模
UNO牌堆包含四种颜色(红、黄、绿、蓝)的数字牌(0-9)与功能牌(跳过、反转、+2),以及万能牌(变色、+4)。出牌规则为:颜色或数字匹配即可打出,万能牌可随时出。
核心数据结构
import java.util.*;
public class UnoGame {
// 颜色枚举
enum Color { RED, YELLOW, GREEN, BLUE, WILD }
// 牌类型
enum Type { NUMBER, SKIP, REVERSE, DRAW_TWO, WILD, WILD_DRAW_FOUR }
static class Card {
Color color;
Type type;
int number; // 数字牌0-9,功能牌为-1
Card(Color color, Type type, int number) {
this.color = color;
this.type = type;
this.number = number;
}
// 判断能否覆盖指定顶牌
boolean canPlayOn(Card top) {
if (color == Color.WILD) return true;
return color == top.color ||
(type == Type.NUMBER && top.type == Type.NUMBER && number == top.number);
}
@Override
public String toString() {
if (color == Color.WILD) return type.name();
if (type == Type.NUMBER) return color + " " + number;
return color + " " + type;
}
}
static class Player {
List<Card> hand = new ArrayList<>();
String name;
Player(String name) { this.name = name; }
// 贪心策略:找出最佳出牌
Card chooseCard(Card topCard, Color currentColor) {
List<Card> playable = new ArrayList<>();
for (Card c : hand) {
if (c.canPlayOn(topCard) || (c.color == Color.WILD)) {
playable.add(c);
}
}
if (playable.isEmpty()) return null;
// 贪心评估:优先保留万能牌,优先出与手牌同色多的颜色
playable.sort((a, b) -> {
int scoreA = evaluateCard(a, topCard, currentColor);
int scoreB = evaluateCard(b, topCard, currentColor);
return scoreB - scoreA; // 分数高优先出
});
return playable.get(0);
}
// 手牌评估函数:分数越低越要保留
int evaluateCard(Card card, Card top, Color currentColor) {
int score = 0;
// 万能牌价值高,尽量保留到最后
if (card.color == Color.WILD) score -= 50;
// +4比变色更珍贵
if (card.type == Type.WILD_DRAW_FOUR) score -= 30;
// 功能牌比数字牌更有用
if (card.type == Type.DRAW_TWO || card.type == Type.SKIP) score += 20;
// 如果手牌中该颜色很多,优先出这种颜色(减少颜色种类)
long colorCount = hand.stream()
.filter(c -> c.color == card.color && c != card).count();
score += (int) colorCount * 5;
// 匹配当前顶牌颜色的优先出
if (card.color == currentColor) score += 10;
return score;
}
// 选择变色后的最优颜色:手牌中最多的颜色
Color chooseBestColor() {
Map<Color, Long> colorCount = new EnumMap<>(Color.class);
for (Card c : hand) {
if (c.color != Color.WILD) {
colorCount.merge(c.color, 1L, Long::sum);
}
}
return colorCount.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse(Color.RED);
}
}
}
牌堆管理与游戏流程
static class Deck {
List<Card> cards = new ArrayList<>();
Deck() {
// 初始化UNO牌堆
for (Color c : new Color[]{Color.RED, Color.YELLOW, Color.GREEN, Color.BLUE}) {
cards.add(new Card(c, Type.NUMBER, 0));
for (int i = 1; i <= 9; i++) {
cards.add(new Card(c, Type.NUMBER, i));
cards.add(new Card(c, Type.NUMBER, i));
}
for (int i = 0; i < 2; i++) {
cards.add(new Card(c, Type.SKIP, -1));
cards.add(new Card(c, Type.REVERSE, -1));
cards.add(new Card(c, Type.DRAW_TWO, -1));
}
}
for (int i = 0; i < 4; i++) {
cards.add(new Card(Color.WILD, Type.WILD, -1));
cards.add(new Card(Color.WILD, Type.WILD_DRAW_FOUR, -1));
}
Collections.shuffle(cards);
}
Card draw() { return cards.isEmpty() ? null : cards.remove(cards.size() - 1); }
}
// 模拟一局游戏,返回获胜者
static Player simulateGame(List<Player> players) {
Deck deck = new Deck();
// 每人发7张牌
for (int i = 0; i < 7; i++) {
for (Player p : players) p.hand.add(deck.draw());
}
Card top = deck.draw();
Color currentColor = top.color;
int direction = 1; // 1顺时针, -1逆时针
int current = 0;
for (int turn = 0; turn < 1000; turn++) { // 防止无限循环
Player p = players.get(current);
Card play = p.chooseCard(top, currentColor);
if (play == null) {
// 无牌可出,抽一张
Card drawn = deck.draw();
if (drawn != null) p.hand.add(drawn);
} else {
p.hand.remove(play);
top = play;
if (play.color == Color.WILD) {
currentColor = p.chooseBestColor();
} else {
currentColor = play.color;
}
// 处理功能牌效果
if (play.type == Type.SKIP) {
current = (current + direction + players.size()) % players.size();
} else if (play.type == Type.REVERSE) {
direction *= -1;
} else if (play.type == Type.DRAW_TWO) {
int next = (current + direction + players.size()) % players.size();
players.get(next).hand.add(deck.draw());
players.get(next).hand.add(deck.draw());
} else if (play.type == Type.WILD_DRAW_FOUR) {
int next = (current + direction + players.size()) % players.size();
for (int i = 0; i < 4; i++) players.get(next).hand.add(deck.draw());
}
if (p.hand.isEmpty()) return p; // 获胜
}
current = (current + direction + players.size()) % players.size();
}
return null;
}
主程序与胜率统计
public static void main(String[] args) {
int rounds = 10000;
Map<String, Integer> wins = new HashMap<>();
for (int i = 0; i < rounds; i++) {
List<Player> players = Arrays.asList(
new Player("贪心AI"),
new Player("随机AI")
);
// 随机AI:随便出一张能出的牌
Player winner = simulateGame(players);
if (winner != null) {
wins.merge(winner.name, 1, Integer::sum);
}
}
System.out.println("=== UNO AI 对战统计 (" + rounds + "局) ===");
wins.forEach((name, count) ->
System.out.printf("%s: %d 胜 (%.2f%%)%n", name, count, 100.0 * count / rounds));
}
}
算法分析
贪心策略设计:AI通过评估函数为每张可出牌打分,优先打出功能牌和高频颜色牌,保留万能牌到最后。这种策略显著优于随机出牌。
时间复杂度:每轮出牌评估为 O(n log n),其中 n 为手牌数量(通常小于20),模拟万局游戏总耗时约1-3秒。
空间复杂度:牌堆固定108张,O(1)。